13 | ;
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/test/config-test.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "http://localhost:8888/"
3 | , "hookURL": "http://localhost:8888/api/hook"
4 | , "hookPath": "api/hook"
5 | , "serverPort": 8888
6 | , "sessionSecret": "I'm a secret"
7 | , "ghClientID": "1234"
8 | , "ghClientSecret": "5678"
9 | , "logToConsole": false
10 | , "logToFile": "test/logs"
11 | , "couchDB": "ashnazg-test"
12 | , "couchAuth": {
13 | "username": "admin",
14 | "password": "password"
15 | }
16 | , "notifyFrom": "test@localhost.test"
17 | , "w3cBotGHToken": "123"
18 | }
19 |
--------------------------------------------------------------------------------
/templates/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | W3C Repository Manager
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | scratch
30 | config.json
31 | data
32 |
33 | # Node.js 8 new shrinkwrap file:
34 | package-lock.json
35 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | name: ash-nazg tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [20, 22, 24]
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Start CouchDB
19 | uses: iamssen/couchdb-github-action@0.3.0
20 | with:
21 | couchdb-version: 3.1
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v2
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - run: npm i
27 | - run: cp ./test/config-test.json ./config.json
28 | - run: node store.js "./test/config-test.json"
29 | - run: npm test
30 |
--------------------------------------------------------------------------------
/templates/CG-contributing.md:
--------------------------------------------------------------------------------
1 | # {{name}}
2 |
3 | This repository is being used for work in the W3C {{name}}, governed by the [W3C Community License
4 | Agreement (CLA)](http://www.w3.org/community/about/process/cla/). To make substantive contributions,
5 | you must join the CG.
6 |
7 | If you are not the sole contributor to a contribution (pull request), please identify all
8 | contributors in the pull request comment.
9 |
10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
11 |
12 | ```
13 | +@github_username
14 | ```
15 |
16 | If you added a contributor by mistake, you can remove them in a comment with:
17 |
18 | ```
19 | -@github_username
20 | ```
21 |
22 | If you are making a pull request on behalf of someone else but you had no part in designing the
23 | feature, you can remove yourself with the above syntax.
24 |
--------------------------------------------------------------------------------
/templates/WG-CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # {{name}}
2 |
3 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the
4 | [W3C Patent Policy](https://www.w3.org/policies/patent-policy/) and
5 | [Document License](https://www.w3.org/copyright/document-license/). To make substantive contributions to specifications, you must either participate
6 | in the relevant W3C Working Group or make a non-member patent licensing commitment.
7 |
8 | If you are not the sole contributor to a contribution (pull request), please identify all
9 | contributors in the pull request comment.
10 |
11 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
12 |
13 | ```
14 | +@github_username
15 | ```
16 |
17 | If you added a contributor by mistake, you can remove them in a comment with:
18 |
19 | ```
20 | -@github_username
21 | ```
22 |
23 | If you are making a pull request on behalf of someone else but you had no part in designing the
24 | feature, you can remove yourself with the above syntax.
25 |
--------------------------------------------------------------------------------
/templates/WG-CONTRIBUTING-SW.md:
--------------------------------------------------------------------------------
1 | # {{name}}
2 |
3 | Contributions to this repository are intended to become part of Recommendation-track documents governed by the
4 | [W3C Patent Policy](https://www.w3.org/policies/patent-policy/) and
5 | [Software and Document License](https://www.w3.org/copyright/software-license/). To make substantive contributions to specifications, you must either participate
6 | in the relevant W3C Working Group or make a non-member patent licensing commitment.
7 |
8 | If you are not the sole contributor to a contribution (pull request), please identify all
9 | contributors in the pull request comment.
10 |
11 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
12 |
13 | ```
14 | +@github_username
15 | ```
16 |
17 | If you added a contributor by mistake, you can remove them in a comment with:
18 |
19 | ```
20 | -@github_username
21 | ```
22 |
23 | If you are making a pull request on behalf of someone else but you had no part in designing the
24 | feature, you can remove yourself with the above syntax.
25 |
--------------------------------------------------------------------------------
/application/utils.js:
--------------------------------------------------------------------------------
1 |
2 | import React from "react";
3 | import MessageActions from "../actions/messages";
4 |
5 | let pathPrefix;
6 |
7 | module.exports = {
8 | jsonHandler: (res) => { return res.json(); }
9 | , catchHandler: (e) => {
10 | MessageActions.error(e);
11 | }
12 | , pathPrefix: () => {
13 | if (!pathPrefix)
14 | pathPrefix = PREFIX;
15 | return pathPrefix;
16 | }
17 | , val: (ref) => {
18 | let el = ref
19 | , value
20 | ;
21 | if (!el) return null;
22 | if (el.multiple) {
23 | value = [];
24 | for (var i = 0, n = el.selectedOptions.length; i < n; i++) {
25 | value.push(el.selectedOptions.item(i).value.trim());
26 | }
27 | }
28 | else value = el.value.trim();
29 | return value;
30 | }
31 | , andify: (list, conjunction = "and") => {
32 | if (list.length === 1) return list[0];
33 | return list.slice(0, -1).join(", ") + " " + conjunction + " " + list.slice(-1);
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Spec proposal
6 |
7 |
17 |
18 |
19 |
20 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 World Wide Web Consortium
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/log.js:
--------------------------------------------------------------------------------
1 |
2 | var winston = require("winston")
3 | , transports = []
4 | ;
5 |
6 | var logger;
7 |
8 | module.exports = function(config) {
9 | if (!logger) {
10 | // logging
11 | if (config.logToConsole) {
12 | transports.push(
13 | new (winston.transports.Console)({
14 | handleExceptions: true
15 | , colorize: true
16 | , maxsize: 200000000
17 | , humanReadableUnhandledException: true
18 | })
19 | );
20 | }
21 | if (config.logToFile) {
22 | transports.push(
23 | new (winston.transports.File)({
24 | filename: config.logToFile
25 | , handleExceptions: true
26 | , timestamp: true
27 | , humanReadableUnhandledException: true
28 | })
29 | );
30 | }
31 |
32 | logger = new (winston.Logger)({ transports: transports });
33 | }
34 | return logger;
35 | };
36 |
--------------------------------------------------------------------------------
/stores/message.js:
--------------------------------------------------------------------------------
1 |
2 | import AshNazgDispatch from "../application/dispatcher";
3 | import EventEmitter from "events";
4 | import assign from "object-assign";
5 |
6 | let _messages = []
7 | , _counter = 0
8 | , MessageStore = module.exports = assign({}, EventEmitter.prototype, {
9 | emitChange: function () { this.emit("change"); }
10 | , addChangeListener: function (cb) { this.on("change", cb); }
11 | , removeChangeListener: function (cb) { this.removeListener("change", cb); }
12 |
13 | , messages: function () {
14 | return _messages;
15 | }
16 | })
17 | ;
18 |
19 | MessageStore.dispatchToken = AshNazgDispatch.register((action) => {
20 | switch (action.type) {
21 | case "error":
22 | case "success":
23 | let msg = typeof action.message === "string" ? action.message : action.message.message;
24 | _messages.push({
25 | id: ++_counter
26 | , message: msg
27 | , type: action.type
28 | });
29 | MessageStore.emitChange();
30 | break;
31 | case "dismiss":
32 | _messages = _messages.filter(function (m) { return m.id != action.id; });
33 | MessageStore.emitChange();
34 | break;
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/application/admin/pick-user.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from "react";
3 | import { Link } from "react-router";
4 |
5 | let utils = require("../../application/utils")
6 | , pp = utils.pathPrefix()
7 | ;
8 |
9 | export default class PickUser extends React.Component {
10 | constructor (props) {
11 | super(props);
12 | this.state = {
13 | username: null
14 | };
15 | }
16 |
17 | handleChange () {
18 | this.setState({ username: utils.val(this.refs.username) });
19 | }
20 |
21 | render () {
22 | let st = this.state
23 | , link = typeof st.username === "string" && st.username.length ?
24 | Pick
25 | :
26 | null
27 | ;
28 | return
29 |
Provide a GitHub user name to add the user
30 |
31 |
32 |
33 | {" "}
34 | {link}
35 |
36 |
37 | ;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/templates/affiliation-mail.txt:
--------------------------------------------------------------------------------
1 | Dear {{displayName}}
2 |
3 | Thank you for submitting a pull request (PR #{{prnum}}) on the W3C specification repository {{repo}}.
4 | https://github.com/{{repo}}/pull/{{prnum}}
5 |
6 | To ensure that the Web can be used and developed by anyone free of charge, W3C develops specifications under a Royalty-Free Patent Policy:
7 | https://www.w3.org/policies/patent-policy/
8 |
9 | As part of this policy, W3C groups must be able to assess the IPR context of contributions made to their repositories.
10 |
11 | As our automated tool was not able to determine with what organization (if any) you are affiliated, we would be very grateful if you could see which of the following applies to you:
12 |
13 | * if your contribution does not concern a normative part of a specification, or is editorial in nature (e.g. fixing typos or examples), you don't need to do anything
14 |
15 | * if you are a member of the group that maintains this repository, please link your W3C and github accounts together at
16 | https://www.w3.org/users/myprofile/connectedaccounts/
17 |
18 | * if you work for a W3C Member organization, please get a W3C account at
19 | https://www.w3.org/account/request/
20 | once that is done, or if you already have a W3C account, please link your W3C and github accounts together at
21 | https://www.w3.org/users/myprofile/connectedaccounts/
22 |
23 | * otherwise, please contact {{contacts}} to see how to proceed with your contribution.
24 |
25 | Thanks again for your contribution. If any of this is unclear, please contact sysreq@w3.org or {{contacts}} for assistance.
26 |
27 | --
28 | W3C automated IPR checker
29 |
--------------------------------------------------------------------------------
/components/flash-list.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from "react";
3 |
4 | // a very simple flash error
5 | export default class FlashList extends React.Component {
6 | // receive a store to listen to
7 | // when it changes, get messages from there
8 | constructor (props) {
9 | super(props);
10 | this.state = { messages: [] };
11 | }
12 | componentDidMount () {
13 | this.props.store.addChangeListener(this._onChange.bind(this));
14 | }
15 | componentWillUnmount () {
16 | this.props.store.removeChangeListener(this._onChange.bind(this));
17 | }
18 | _onChange () {
19 | this.setState({ messages: this.props.store.messages() });
20 | }
21 | dismiss (id) {
22 | this.props.actions.dismiss(id);
23 | }
24 |
25 | render () {
26 | let st = this.state
27 | , messages = st.messages || []
28 | ;
29 | return
34 | This site is essentially an application built on top of GitHub. As such,
35 | in order for it to work, you need to log into it using your GitHub
36 | credentials.
37 |
63 | Use this interface to grant administrative privileges to users and set their
64 | affiliations (both to groups and to members). Be careful, admins are
65 | considered to be reliable people, they can break things.
66 |
67 |
68 | The “blanket” status is granted to users who are thereby considered to be
69 | authorised for all pull requests, without needing to be part of a given
70 | group. This is typically restricted to W3C Staff.
71 |
86 | Use this interface to add W3C groups into the system such that they may be
87 | managed for repositories and the such. Please don't add groups unless you
88 | need to as we'd like to keep various UI drop-downs within sane sizes.
89 |
112 | ;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/public/css/app.min.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}@font-face{font-family:Titillium;src:url(../fonts/titilliumweb-extralight-webfont.eot);src:url(../fonts/titilliumweb-extralight-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/titilliumweb-extralight-webfont.woff2) format("woff2"),url(../fonts/titilliumweb-extralight-webfont.woff) format("woff"),url(../fonts/titilliumweb-extralight-webfont.ttf) format("truetype");font-weight:200;font-style:normal}@font-face{font-family:Titillium;src:url(../fonts/titilliumweb-light-webfont.eot);src:url(../fonts/titilliumweb-light-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/titilliumweb-light-webfont.woff2) format("woff2"),url(../fonts/titilliumweb-light-webfont.woff) format("woff"),url(../fonts/titilliumweb-light-webfont.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Titillium;src:url(../fonts/titilliumweb-lightitalic-webfont.eot);src:url(../fonts/titilliumweb-lightitalic-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/titilliumweb-lightitalic-webfont.woff2) format("woff2"),url(../fonts/titilliumweb-lightitalic-webfont.woff) format("woff"),url(../fonts/titilliumweb-lightitalic-webfont.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Titillium;src:url(../fonts/titilliumweb-semibold-webfont.eot);src:url(../fonts/titilliumweb-semibold-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/titilliumweb-semibold-webfont.woff2) format("woff2"),url(../fonts/titilliumweb-semibold-webfont.woff) format("woff"),url(../fonts/titilliumweb-semibold-webfont.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Titillium;src:url(../fonts/titilliumweb-semibolditalic-webfont.eot);src:url(../fonts/titilliumweb-semibolditalic-webfont.eot?#iefix) format("embedded-opentype"),url(../fonts/titilliumweb-semibolditalic-webfont.woff2) format("woff2"),url(../fonts/titilliumweb-semibolditalic-webfont.woff) format("woff"),url(../fonts/titilliumweb-semibolditalic-webfont.ttf) format("truetype");font-weight:700;font-style:italic}@media (min-width:30em){.row{width:100%;display:table;table-layout:fixed}.col{display:table-cell}}body,html{width:100%;height:100%;margin:0;padding:0;font-family:Titillium}header{border-bottom:1px solid silver;padding:0 0 0 10px}div.app-body,footer,h1{max-width:960px;margin:auto}h1{color:#b13737;font-size:50px;font-weight:300;padding:0 0 0 10px}h2{font-weight:300;font-size:30px;margin-bottom:10px}a[href]{color:#b13737}a[href]:hover{color:#036}.col.nav{width:200px;padding:0 10px}.nav-box{margin-bottom:20px}.nav-box-header{font-weight:700;color:#a9a9a9;border-bottom:1px solid silver}.nav-box ul{list-style-type:none;padding:0;margin:0}.nav-box li a,.nav-box li button{color:#b13737;display:block;width:100%;text-align:left;padding:5px;text-decoration:none;cursor:pointer;background:0 0;border:none;font:inherit}.nav-box li a:hover,.nav-box li button:hover{background:#b13737;color:#fff}.nav-box li a.active,.nav-box li button:active{font-weight:700}.primary-app{padding-left:30px}.login-box{width:350px;border:1px solid silver;padding:10px 20px}.formline{width:auto;padding-bottom:10px}.formline label{display:block;font-weight:700}.formline label.inline{display:inline-block;font-weight:400}.formline.actions{text-align:right}.spinner{padding:20px;text-align:center}th{text-align:left}td,th{padding:5px;vertical-align:top}tr{border-bottom:1px solid silver}tbody tr:nth-of-type(even){background:#eee}td>ul{margin:0;padding-left:20px}a.button,button{background:#b13737;border:none;border-radius:5px;color:#fff;text-decoration:none;padding:0 5px;cursor:pointer}button:disabled{background:silver;color:#333}.flash-list{margin-left:230px}.flash-list button{position:absolute;top:10px;right:10px;color:#fff}.flash-list p{margin:0}.flash-error,.flash-success{border-radius:5px;padding:20px;margin-top:10px;position:relative}.flash-success{border:1px solid green}.flash-error{border:1px solid #df5d5d}.flash-success button{background:green}.flash-error button{background:#df5d5d}.good{color:green}.bad{color:red}td.bad,td.good{font-weight:700}.users-list .admin{color:#333}.groups-list .managed{font-weight:700}
--------------------------------------------------------------------------------
/app.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Router, Route, Link } from "react-router";
4 | import {browserHistory } from "react-router";
5 | import ReactDOM from "react-dom";
6 |
7 | import Application from "./components/application.jsx";
8 | import Row from "./components/row.jsx";
9 | import Col from "./components/col.jsx";
10 | import NavBox from "./components/nav-box.jsx";
11 | import NavItem from "./components/nav-item.jsx";
12 | import Spinner from "./components/spinner.jsx";
13 | import FlashList from "./components/flash-list.jsx";
14 |
15 | import UserActions from "./actions/user";
16 | import LoginStore from "./stores/login";
17 |
18 | import MessageActions from "./actions/messages";
19 | import MessageStore from "./stores/message";
20 |
21 | import Welcome from "./application/welcome.jsx";
22 | import LoginWelcome from "./application/login.jsx";
23 | import LogoutButton from "./application/logout-button.jsx";
24 | import RepoManager from "./application/repo-manager.jsx";
25 | import RepoList from "./application/repo-list.jsx";
26 | import ContributorsList from "./application/contributors-list.jsx";
27 | import AdminUsers from "./application/admin/users.jsx";
28 | import AdminGroups from "./application/admin/groups.jsx";
29 | import EditUser from "./application/admin/edit-user.jsx";
30 | import PickUser from "./application/admin/pick-user.jsx";
31 | import AddUser from "./application/admin/add-user.jsx";
32 | import PRViewer from "./application/pr/viewer.jsx";
33 | import PROpen from "./application/pr/open.jsx";
34 | import PRLastWeek from "./application/pr/last-week.jsx";
35 |
36 | let utils = require("./application/utils")
37 | , pp = utils.pathPrefix()
38 | ;
39 |
40 | function getState () {
41 | return { loggedIn: LoginStore.isLoggedIn(), admin: LoginStore.isAdmin(), importGranted: LoginStore.isImportGranted() };
42 | }
43 |
44 | class AshNazg extends React.Component {
45 | constructor (props) {
46 | super(props);
47 | this.state = getState();
48 | }
49 | componentDidMount () {
50 | LoginStore.addChangeListener(this._onChange.bind(this));
51 | UserActions.login();
52 | }
53 | componentWillUnmount () {
54 | LoginStore.removeChangeListener(this._onChange.bind(this));
55 | }
56 | _onChange () {
57 | this.setState(getState());
58 | }
59 |
60 | render () {
61 | let st = this.state
62 | , nav
63 | , body
64 | , admin
65 | , repoNav
66 | , userNav
67 | ;
68 | // show admin links as well
69 | if (st.admin) {
70 | admin =
71 | Users
72 | Add User
73 | Groups
74 |
75 | ;
76 | }
77 | if (st.importGranted) {
78 | repoNav =
79 | New Repository
80 | Import Repository
81 |
82 | ;
83 | }
84 |
85 | if (st.loggedIn) {
86 | userNav =
87 |
88 |
89 | ;
90 | } else {
91 | userNav = Login;
92 | }
93 | // when logged in show an actual menu and content
94 | const isRoutePublic = this.props.routes[this.props.routes.length - 1].public;
95 | if (st.loggedIn === true || isRoutePublic) {
96 | nav =
97 |
98 | List Repositories
99 | {repoNav}
100 |
101 |
102 | Currently Open
103 | Active Last Week
104 |
105 | {admin}
106 | {userNav}
107 | ;
108 | body =
{ renderChildrenWithAdminProp(this.props.children, st.admin) || };
109 | }
110 | // when logged out off to log in
111 | else if (st.loggedIn === false) {
112 | nav =
;
113 | body =
;
114 | }
115 | // while we don't know if we're logged in or out, spinner
116 | else {
117 | body =
161 | ;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Repository Manager (Ash-Nazg)
3 |
4 | One interface to find all group contributors and in Intellectual Property Rights (IPR) bind them.
5 |
6 | This tool was created to support contributions made to a group, under the form of pull requests, in
7 | order to assess whether they are IPR-OK or not. It still has some rough edges but hopefully it can
8 | be usable enough to get started, and perfected over time.
9 |
10 | The tool is at currently in [my labs hatchery](https://labs.w3.org/hatchery/repo-manager/), but
11 | hopefully at some point some kind soul will place it at a more memorable URL.
12 |
13 | When you get there, you will be asked to log in through GitHub. You can't do much without that,
14 | because most of the actions you can undertake through the tool (or that the tool can undertake on
15 | your behalf when reacting to a GitHub event) require authorised access to GitHub. The permissions
16 | it requires are rather broad; that is because it is difficult to be granular with GitHub
17 | permissions. The tool isn't doing anything unholy.
18 |
19 | Once you log in your user will be created; if you need to be an admin just ask someone to give you
20 | that flag from the "Edit User" page. *Note:* there are currently two distinct login flows. Some features
21 | such as create and import repositories do not appear unless you sign in via the second link.
22 |
23 | If you need to deploy or to hack on this tool, you will want to read the
24 | [Development Guide](https://github.com/w3c/ash-nazg/blob/master/DEVELOPMENT.md)
25 |
26 | ## Common Tool
27 |
28 | ### [New Repository](https://labs.w3.org/repo-manager/repo/new)
29 |
30 | This is basically what people should use when they want to start a new specification with the WG/CG.
31 | It gives you a choice of the organizations under which you are allowed to create a new repo
32 | (including your own user), and you can pick the name of the repo and the groups to which it
33 | belongs.
34 |
35 | *Note*: the list of organizations depends on the user's GitHub organizations. If you are owner of an
36 | organization and you don't see it in the list, you need to grant the repository manager access to that
37 | organization. To do so, go in your
38 | ['Authorized OAuth Apps' settings`](https://github.com/settings/applications), click on 'W3C Repository
39 | Manager' and grant access to the new organization.
40 |
41 | Hitting "Create" can take a little while as the tool does all of the following, live:
42 |
43 | * Creates the repo on GitHub
44 | * Adds several files, notably the `LICENSE.md` and `CONTRIBUTE.md`, a `w3c.json` file which can be
45 | used by other tools, and an `index.html` that's a bare-bones ReSpec spec ready to be edited.
46 | * Adds a hook to the repo such that pull requests and comments on them are sent to us, including one
47 | distinct cryptographic secret per repo.
48 | * Saves all the relevant info on our side.
49 |
50 | Most users should only ever have to use that. Once done they can go and play in their repo.
51 |
52 | **Important**: [`w3cbot`](https://github.com/w3cbot) should be able to comment on the different pull
53 | requests so you should consider adding @w3cbot as a member of the organization.
54 |
55 | ### [Import Repository](https://labs.w3.org/repo-manager/repo/import)
56 |
57 | This is the same as "New" but for an existing repo. It will ***never*** overwrite something there so
58 | it is the user's responsibility to check that the repo is okay once imported.
59 |
60 | ### Logout
61 |
62 | This should be obvious. If it isn't, please don't use the application.
63 |
64 | ### How Pull Requests Get Handled
65 |
66 | Whenever a pull request is made against a repo that is under the tool's management, we get notified.
67 | We use this information to assess if the PR is acceptable (i.e. has all its contributors in at least
68 | one of the groups that the repo belongs to).
69 |
70 | Count as contributors not just the person making the pull request, but also anyone added either in
71 | the PR description or in any subsequent comment using "`+@username`" on a line on its own. If a
72 | contributor was added by mistake, she can be removed with "`-@username`" on a line on its own. This
73 | includes the person making the PR. Thanks to that, you can issue a PR completely on behalf of
74 | someone else.
75 |
76 | Every time a PR is created or has a comment with a username change, the status of the PR is changed.
77 | If it's acceptable it'll get changed to green with a note indicating that it's fine; if not it gets
78 | changed to some ugly brown with a red cross (and a link that people can use to check the issue in
79 | more detail).
80 |
81 |
82 | ## Admin Tools
83 |
84 | ### Currently Open Pull Requests
85 |
86 | This list all PRs that are now open, even old ones. It lists useful details such as which users are
87 | being problematic either because they are unknown (not in the system at all) or outside (known to
88 | the system but not in one of the right groups for that repo).
89 |
90 | You can go to PR details by clicking "Details".
91 |
92 | ### PR Details
93 |
94 | If the PR is not in an acceptable state, this will list problematic users with a link to fix them
95 | each. The fix can either be "Add to system" or "Edit" (details below).
96 |
97 | The idea is that the vast majority of non-acceptable PRs in the first few weeks will come from
98 | people who are simply not known, but that relatively quickly it ought to become a less frequent
99 | occurrence.
100 |
101 | If it so happens that all of the problematic users can be added to the system or to the right group,
102 | and that you have done so, then you can return to the PR details page and hit "Revalidate". We could
103 | revalidate every time a user is added or edited, but it's pretty costly so for the time being it is
104 | done this way. Revalidation will of course update both the local state and the PR's mergability
105 | indicator on GitHub.
106 |
107 | ### Add User to system
108 |
109 | For users that are unknown to the system, they can be added by following on of those links and just
110 | clicking that button. This is always an innocuous operation; it does not give the user any special
111 | rights nor can it make a PR OK (since the user needs to be in a group for that).
112 |
113 | ### Active Last Week PRs
114 |
115 | This is a list of pull requests, in any state, that saw activity last week. They can be filtered
116 | according to the affiliation of the companies that made the contributions. This is essentially so
117 | that AC reps who have people in CGs who are only supposed to contribute to some specific work but
118 | not all of it can monitor what's been going on and avail themselves of their 45 days retraction
119 | window. Similar affordances are available as for the list of open PRs.
120 |
121 | ### Edit User
122 |
123 | The interface to edit users is where the W3C data model and the GitHub data model get to meet. This
124 | alone is scary; I've tried to make it less scary.
125 |
126 | A list of the groups known to the system is shown, the user can be added and removed from them
127 | there. If the user's affiliation is unset, once some groups have been added you can click "Set".
128 | This will load up a list that is the *intersection* of membership in the selected groups. The UI
129 | will also try to select the user with the name matching their GitHub profile (which may not always
130 | work, but often does). Hitting "OK" will associate the GH user with the W3C user, making it possible
131 | for us to use affiliation information. Don't forget to hit "Save".
132 |
133 | This is a little convoluted but it's the best I could do with the current APIs from both GitHub and
134 | the W3C backend. Hopefully it can be simplified in the future.
135 |
136 | ### Admin > Users
137 |
138 | This is a list of users. Things you can do there include making them admins and giving them blanket
139 | contribution rights. **USE EITHER WITH CARE**.
140 |
141 | Admins should normally not be able to break the system, but they can enter all sorts of bogus
142 | information that would be really annoying. Only grant admin when you're sure; it's probably better
143 | to ask others first.
144 |
145 | Blanket is a different type of superpower: users with blanket access are considered acceptable
146 | contributors to ALL repos, irrespective of their group memberships. This should normally be
147 | restricted to W3C team people.
148 |
149 | ### Admin > Groups
150 |
151 | This is a list of all W3C groups. You will note that most have an "Add" button next to them: those
152 | are the ones that are in W3C but not in this system. Please do *not* start adding groups unless they
153 | explicitly want to be managed under this system. We only want people to create/import repos for
154 | groups that are actually using this system. Clicking "Add" makes that group one of those available
155 | for repos and users to belong to, adding too many will make those dialogs unwieldy.
156 |
157 | Share & Enjoy!
158 |
--------------------------------------------------------------------------------
/pr-check.js:
--------------------------------------------------------------------------------
1 | const async = require("async")
2 | , notification = require('./notification')
3 | , w3ciprcheck = require('./w3c-ipr')
4 | , doAsync = require('doasync') // rather than utils.promisy to get "free" support for object methods
5 | , w3c = require("node-w3capi")
6 | , types = {
7 | 'working group': 'wg',
8 | 'interest group': 'ig',
9 | 'community group': 'cg',
10 | 'business group': 'bg'
11 | };
12 |
13 | let store, log;
14 |
15 | async function findW3CUserFromGithub(user) {
16 | log.info("Looking for github user with id " + user.ghID + " in W3C API");
17 | try {
18 | let w3cuser = await w3c.user({type: 'github', id: user.ghID}).fetch();
19 | log.info(JSON.stringify(w3cuser, null, 2));
20 | await doAsync(store).mergeOnUser(user.username, {
21 | w3cid: w3cuser.id,
22 | w3capi: w3cuser._links.self.href.replace(/.*\//, "")
23 | });
24 | } catch (err) {
25 | return user;
26 | }
27 | log.info("Found matching W3C user");
28 | return doAsync(store).getUser(user.username);
29 | }
30 |
31 | async function findOrCreateUserFromGithub(username, gh) {
32 | let user;
33 | try {
34 | user = await doAsync(store).getUser(username);
35 | } catch (err) {
36 | if (err.error !== "not_found") throw err;
37 | }
38 | if (!user) {
39 | log.info("Getting GH id from github for " + username);
40 | let ghuser = await gh.getUser(username);
41 | // we store this for sake of efficiency
42 | await doAsync(store).addUser(ghuser);
43 | return findW3CUserFromGithub(ghuser);
44 | } else {
45 | // Let's check if the link has since been established
46 | if (!user.w3capi) {
47 | return findW3CUserFromGithub(user);
48 | } else {
49 | return user;
50 | }
51 | }
52 | }
53 |
54 |
55 | async function getStoredPR(fullname) {
56 | log.info("Setting status for PR " + fullname);
57 | let repo = await doAsync(store).getRepo(fullname);
58 | if (!repo) throw ("Unknown repository: " + fullname);
59 | let token = await doAsync(store).getToken(repo.owner);
60 | if (!token) throw ("Token not found for: " + repo.owner);
61 | return {repoGroups: repo.groups, token};
62 | }
63 |
64 | async function updateStoredPR(pr) {
65 | log.info("Setting status for PR " + pr.fullName);
66 | await doAsync(store).updatePR(pr.fullName, pr.num, pr);
67 | return pr;
68 | }
69 |
70 | async function setGhStatus(gh, status) {
71 | return new Promise((res, rej) => {
72 | gh.status(status, (err) => {
73 | if (err) log.error(err);
74 | res();
75 | })
76 | });
77 | }
78 |
79 | async function checkPrScope(gh, pr) {
80 | const ignoreFiles = ["package.json", "package-lock.json", ".travis.yml", "w3c.json", "CONTRIBUTING.md", "LICENSE.md", "LICENSE.txt", "CODE_OF_CONDUCT.md"];
81 | const ignorePath = ".github/";
82 | let files;
83 | try {
84 | files = await gh.getPrFiles(pr.owner, pr.shortName, pr.num);
85 | } catch(err) {
86 | log.error(err);
87 | // if unsure, assumes it is IPR-relevant
88 | return true;
89 | }
90 | return !(files.map(f => f.filename).every(p => ignoreFiles.includes(p) || p.startsWith(ignorePath)));
91 | }
92 |
93 | function prChecker(config, argLog, argStore, GH, mailer) {
94 | log = argLog;
95 | store = argStore;
96 | return {
97 | validate: async function prStatus (pr, delta, cb) {
98 | const currentPrAcceptability = pr.acceptable;
99 | const prString = pr.owner + "/" + pr.shortName + "/" + pr.num;
100 | const statusData = {
101 | owner: pr.owner,
102 | shortName: pr.shortName,
103 | sha: pr.sha,
104 | payload: {
105 | state: "pending",
106 | target_url: config.url + "pr/id/" + prString,
107 | description: "PR is being assessed, results will come shortly.",
108 | context: "ipr"
109 | }
110 | };
111 | let token, repoGroups, iprRelevant = true;
112 | try {
113 | ({token, repoGroups} = await getStoredPR(pr.fullName));
114 | } catch (err) {
115 | return cb(err);
116 | }
117 | const gh = new GH({ accessToken: token.token });
118 | log.info("Setting pending status on " + prString);
119 | await setGhStatus(gh, statusData);
120 |
121 | iprRelevant = await checkPrScope(gh, pr, log);
122 | if (!iprRelevant) {
123 | statusData.payload.state = "success";
124 | statusData.payload.description = "PR files identified as non-substantive.";
125 | log.info("Setting status success for " + prString);
126 | pr.acceptable = "yes";
127 | await setGhStatus(gh, statusData);
128 | try {
129 | let updatedPR = await updateStoredPR(pr);
130 | return cb(null, updatedPR);
131 | } catch (err) {
132 | return cb(err);
133 | }
134 | }
135 |
136 | if (pr.markedAsNonSubstantiveBy) {
137 | pr.acceptable = "yes";
138 | statusData.payload.state = "success";
139 | statusData.payload.description = "PR deemed acceptable as non-substantive by @" + pr.markedAsNonSubstantiveBy + ".";
140 | log.info("Setting status success for " + prString);
141 | await setGhStatus(gh, statusData);
142 | try {
143 | let updatedPR = await updateStoredPR(pr);
144 | return cb(null, updatedPR);
145 | } catch (err) {
146 | return cb(err);
147 | }
148 | }
149 |
150 | log.info("Looking up users for " + prString);
151 | let contrib = {};
152 | log.info("Finding deltas for " + prString);
153 | pr.contributors.forEach(function (c) { contrib[c] = true; });
154 | delta.add.forEach(function (c) { contrib[c] = true; });
155 | delta.remove.forEach(function (c) { delete contrib[c]; });
156 | pr.contributors = Object.keys(contrib);
157 | pr.contribStatus = {};
158 | pr.groups = repoGroups;
159 | pr.affiliations = {};
160 | let results = await Promise.all(
161 | pr.contributors.map(async function(username) {
162 | let user = await findOrCreateUserFromGithub(username, gh);
163 | // TODO: check that this is appropriate
164 | // and if so, replace by check of affiliation
165 | // to staff
166 | if (user.blanket) {
167 | pr.affiliations[user.affiliation] = user.affiliationName;
168 | pr.contribStatus[username] = "ok";
169 | return "ok";
170 | }
171 | // if user not found in W3C API,
172 | // report undetermined affiliation
173 | // TODO: We will contact contributor to ask
174 | // establishing the connection.
175 | if (!user.w3capi) {
176 | pr.contribStatus[username] = "undetermined affiliation";
177 | return "undetermined affiliation";
178 | }
179 |
180 | let groups = [];
181 |
182 | for (let g of repoGroups) {
183 | // get group type and shortname
184 | try {
185 | const group = await w3c.group(g).fetch();
186 | groups.push({id: g, type: types[group.type], shortname: group.shortname});
187 | } catch (err) {
188 | return cb(err);
189 | }
190 | }
191 |
192 | let result = await w3ciprcheck(w3c, user.w3capi, user.displayName, groups, store);
193 | let ok = result.ok;
194 | if (ok) {
195 | pr.affiliations[result.affiliation.id] = result.affiliation.name;
196 | pr.contribStatus[username] = "ok";
197 | return "ok";
198 | } else {
199 | // we assume that all groups are of the same type
200 | let group = await doAsync(store).getGroup(repoGroups[0]);
201 | if (!group) throw "Unknown group: " + repoGroups[0];
202 | if (group.groupType === 'WG') {
203 | log.info("Looking up for non-participant licensing contribution");
204 | if (pr.repoId) {
205 | let nplc;
206 | try {
207 | nplc = await w3c.nplc({repoId: pr.repoId, pr: pr.num}).fetch();
208 | } catch (err) {
209 | // Non-participant licensing contribution doesn't exist
210 | pr.contribStatus[username] = "not in group";
211 | return "not in group";
212 | }
213 | const u = nplc.commitments.find(c => c.user["connected-accounts"].find(ca => ca.nickname === username));
214 | const contribStatus = (u.commitment_date === undefined) ? "commitment pending" : "ok";
215 | pr.contribStatus[username] = contribStatus;
216 | return contribStatus;
217 | } else {
218 | pr.contribStatus[username] = "no commitment made - missing repository ID";
219 | return "no commitment made - missing repository ID";
220 | }
221 | } else {
222 | pr.contribStatus[username] = "not in group";
223 | return "not in group";
224 | }
225 | }
226 | }));
227 | let good = results.every(st => st === "ok");
228 | log.info("Got users for " + prString + " results good? " + good);
229 | if (good) {
230 | pr.acceptable = "yes";
231 | pr.unknownUsers = [];
232 | pr.outsideUsers = [];
233 | statusData.payload.state = "success";
234 | statusData.payload.description = "PR deemed acceptable.";
235 | log.info("Setting status success for " + prString);
236 | await setGhStatus(gh, statusData);
237 | let updatedPR = await updateStoredPR(pr);
238 | return cb(null, updatedPR);
239 | }
240 | pr.acceptable = "no";
241 | pr.unknownUsers = [];
242 | pr.outsideUsers = [];
243 | pr.unaffiliatedUsers = [];
244 | for (var u in pr.contribStatus) {
245 | if (pr.contribStatus[u] === "unknown") pr.unknownUsers.push(u);
246 | if (pr.contribStatus[u] === "undetermined affiliation") pr.unaffiliatedUsers.push(u);
247 | if (pr.contribStatus[u] === "not in group") pr.outsideUsers.push(u);
248 | }
249 | var msg = "PR has contribution issues.";
250 | const collateUserNames = users => users.map(u => "@" + u).join (", ");
251 | if (pr.unknownUsers.length)
252 | msg += " The following users were unknown: " + collateUserNames(pr.unknownUsers) +
253 | ".";
254 | if (pr.unaffiliatedUsers.length)
255 | msg += " The following users' affiliation could not be determined: " + collateUserNames(pr.unaffiliatedUsers) + ".";
256 | if (pr.outsideUsers.length)
257 | msg += " The following users were not in the repository's groups: " + collateUserNames(pr.outsideUsers) + ".";
258 | statusData.payload.state = "failure";
259 | statusData.payload.description = msg;
260 | if (statusData.payload.description.length > 140) {
261 | statusData.payload.description = statusData.payload.description.slice(0, 139) + '…';
262 | }
263 | log.info("Setting status failure for " + prString + ", " + msg);
264 | await setGhStatus(gh, statusData);
265 | let updatedPR = await updateStoredPR(pr);
266 | // Only send email notifications
267 | // if the status of the PR has just
268 | // changed
269 | if (currentPrAcceptability !== pr.acceptable) {
270 | // FIXME: make it less context-dependent
271 | await notification.notifyContacts(gh, pr, statusData, mailer, {from: config.notifyFrom, fallback: config.emailFallback || [], cc: config.emailCC || []}, store, log);
272 | return cb(null, updatedPR);
273 | }
274 | return cb(null, updatedPR);
275 | }
276 | };
277 | }
278 |
279 | module.exports = prChecker;
280 |
--------------------------------------------------------------------------------
/application/pr/viewer.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from "react";
3 | import Spinner from "../../components/spinner.jsx";
4 | import { Link } from "react-router";
5 | import MessageActions from "../../actions/messages";
6 |
7 | require("isomorphic-fetch");
8 | let utils = require("../../application/utils")
9 | , pp = utils.pathPrefix()
10 | ;
11 |
12 | export default class PRViewer extends React.Component {
13 | constructor (props) {
14 | super(props);
15 | this.state = {
16 | status: "loading"
17 | , pr: null
18 | , owner: null
19 | , shortName: null
20 | , num: null
21 | , groupDetails: []
22 | , isTeamcontact: null
23 | };
24 | }
25 | componentDidMount () {
26 | let owner = this.props.params.owner
27 | , shortName = this.props.params.shortName
28 | , num = this.props.params.num
29 | , groupDetails
30 | , isTeamcontact = false
31 | ;
32 | this.setState({ owner: owner, shortName: shortName, num: num });
33 | fetch(pp + "api/pr/" + [owner, shortName, num].join("/"), { credentials: "include" })
34 | .then(utils.jsonHandler)
35 | .then((data) => {
36 | groupDetails = data.groupDetails || [];
37 | this.setState({ pr: data });
38 | })
39 | .then(() => {
40 | const types = {
41 | 'working group': 'wg',
42 | 'interest group': 'ig',
43 | 'community group': 'cg',
44 | 'business group': 'bg'
45 | };
46 | return Promise.all(groupDetails
47 | .map(g => fetch(pp + "api/w3c/group/" + g.w3cid)
48 | .then(utils.jsonHandler)
49 | .then(groupdata => {
50 | const group = groupDetails.find(gg => gg.w3cid === g.w3cid);
51 | group.joinhref = groupdata._links.join.href;
52 | group.shortname = groupdata.shortname;
53 | group.type = types[groupdata.type];
54 | })
55 | ));
56 | })
57 | .then(() => fetch(pp + "api/team-contact-of", { credentials: "include"})
58 | .then(utils.jsonHandler)
59 | .then((data) => {
60 | if (!data.hasOwnProperty('error')) {
61 | isTeamcontact = data.some(wg => groupDetails.map(g => `https://api.w3.org/groups/${g.type}/${g.shortname}`).includes(wg.href));
62 | }
63 | })
64 | )
65 | .then(() => this.setState({groupDetails, status: "ready", isTeamcontact: isTeamcontact}))
66 | .catch(utils.catchHandler)
67 | ;
68 |
69 | }
70 |
71 | revalidate () {
72 | let st = this.state;
73 | this.setState({ status: "loading" });
74 | fetch(pp + "api/pr/" + [st.owner, st.shortName, st.num, "revalidate"].join("/"), { method: "POST", credentials: "include" })
75 | .then(utils.jsonHandler)
76 | .then((data) => {
77 | console.log("got data", data);
78 | this.setState({ pr: data, status: "ready" });
79 | if (data.error) return MessageActions.error(data.error);
80 | })
81 | .catch(utils.catchHandler)
82 | ;
83 | }
84 |
85 | markSubstantiveOrNot (ev) {
86 | const nonsubstantive = ev.target.name === "nonsubstantive";
87 | let st = this.state;
88 | this.setState({ status: "loading" });
89 | fetch(pp + "api/pr/" + [st.owner, st.shortName, st.num, "markAs" + (nonsubstantive ? "Non" : "") + "Substantive"].join("/"), { method: "POST", credentials: "include" })
90 | .then(utils.jsonHandler)
91 | .then((data) => {
92 | this.setState({ pr: data, status: "ready" });
93 | if (data.error) return MessageActions.error(data.error);
94 | })
95 | .catch(utils.catchHandler)
96 | ;
97 |
98 | }
99 |
100 | render () {
101 | let st = this.state
102 | , content
103 | , link
104 | , doc
105 | ;
106 | if (st.status === "loading") {
107 | content = ;
108 | link = "loading";
109 | }
110 | else if (st.status === "ready") {
111 | let cs = st.pr.contribStatus || {}
112 | , thStyle = { paddingRight: "20px" }
113 | ;
114 | link =
115 | {st.owner + "/" + st.shortName + "#" + st.num}
116 |
117 | ;
118 | let action;
119 | const prAcceptance = st.pr.acceptable === "yes" ? (st.pr.markedAsNonSubstantiveBy ? "Marked as non substantive by " + st.pr.markedAsNonSubstantiveBy : "Made with proper IPR commitments" ) : "no";
120 | if (st.pr.acceptable === "yes") {
121 | let revert = "";
122 | if (st.pr.markedAsNonSubstantiveBy) {
123 | revert = ℹ ;
124 | }
125 | let merge = "";
126 | if (st.pr.status === "open") {
127 | merge = go merge it at {link};
128 | }
129 | action = {revert}{ revert && merge ? " — or " : (merge ? " — " : "")}{merge};
130 | } else {
131 | let nplc;
132 | if ((this.props.isAdmin || st.isTeamcontact) && st.pr.repoId && !Object.keys(cs).some(u => cs[u] === "undetermined affiliation")) {
133 | let st = this.state
134 | , nplcUrl = new URL(['/standards/licensing/contributions', st.pr.repoId, st.num, 'edit/'].join("/"), 'https://www.w3.org/')
135 | , qs = st.pr.contributors.map(c => 'contributors[]=' + c).concat(st.pr.groups.map(g => 'groups[]=' + g)).join('&')
136 | ;
137 | nplcUrl.search = qs;
138 | nplc = Ask for non-participant commitment
139 | }
140 | action = ℹ {nplc};
141 | }
142 | content =
169 | {username} needs to submit their non-participant licensing commitment via the link they received by email.
170 |
171 | ;
172 | }
173 | else {
174 | const groupJoins = (st.groupDetails || []).map((g, i, a) => join the {g.name} {i < a.length - 1 ? " or " : ""});
175 | return
176 | {username} did not make IPR commitments for this group. To make the IPR commitments, {username} should {groupJoins}.
177 |
;
178 | }
179 | })
180 | }
181 |
182 |
183 |
184 |
185 | ;
186 | if (st.pr.acceptable == "no" && st.pr.unaffiliatedUsers.length) {
187 | var groupDoc, groups = utils.andify(st.groupDetails.map(g => g.name), "or");
188 | // we assume that all groups are of the same type
189 | if (!st.groupDetails || !st.groupDetails.length || st.groupDetails[0].groupType === 'WG') {
190 | let instructions = instructions
191 | groupDoc = [
Otherwise, the WG’s team contacts will request the contributors to sign the non-participant licensing commitments{!st.pr.repoId ? " (missing repository ID in the database)" : ""}{(this.props.isAdmin || st.isTeamcontact) ? [" - ", instructions] : ""}
]
193 | } else {
194 | groupDoc =
Otherwise, the group’s chairs will need to figure how to get the proper IPR commitment from the contributor.
195 | }
196 |
197 | doc =
198 |
Some of the contributors in this pull request were not recognized as having made the required IPR commitment to make substantive changes to the specification in this repository.
199 |
To fix this situation, please see which of the following applies:
200 |
201 |
if the contribution does not concern a normative part of a specification, or is editorial in nature (e.g. fixing typos or examples), the contribution can be marked as non-substantive with the button above - this requires to be logged-in in this system.
279 | Use this interface to set a the group and company affiliation for a user.
280 | The process is a little baroque due to the nature of the APIs queried for
281 | this purpose: a user needs to be associated with (at least) one group in
282 | order for their W3C ID to be discoverable, and through that the matching
283 | affiliation.
284 |
328 | Use the form below to create a new repository under either your user or one
329 | of the organisations that you have write access to. There is no requirement
330 | to place your proposal under the w3c organisation; in fact if
331 | a proposal is simply your own, using your personal repository is preferred.
332 | No preference is given to a specification proposal based on the user or
333 | organisation it belongs to.
334 |
344 | Use the form below to update the owner/name of the repository and/or the group(s) to which this repository is associated with.
345 |
346 | {content}
347 | {results}
348 |
349 | ;
350 | }
351 | else {
352 | return
353 |
Import Repository
354 |
355 | Use this form to turn an unmanaged repository into a W3C-managed repository. This tool does the following:
356 |
357 |
associates the repository with one or more groups (for patent policy and other integration)
358 |
checks that pull requests made on the repository from now on match the IPR commitments of the submitters
359 |
makes it easy to add files that are important to group work (e.g., the code of conduct)
360 |
361 |
362 | {content}
363 | {results}
364 |
365 | ;
366 | }
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 |
2 | # How to develop Ash-Nazg
3 |
4 | This document describes what one needs to know in order to hack on Ash-Nazg. If you are familiar
5 | with Node, [CouchDB][CouchDB], and [React][React] you are already on sane territory but I recommend
6 | you at least skim this document as the local specificities are laid out as well.
7 |
8 | ## IMPORTANT WARNING
9 |
10 | If you are rebuilding the client-side code on a Mac, you are likely to get an incomprehensible error
11 | from [Browserify][Browserify] of the type `Error: EMFILE, open '/some/path'`. That is because the
12 | number of simultaneously open files is bizarrely low on OSX, and Browserify opens a bizarrely high
13 | number of resources concurrently.
14 |
15 | In order to do that, in the environment that runs the build, you will need to run:
16 |
17 | ulimit -n 2560
18 |
19 | If you don't know that, you can waste quite some time.
20 |
21 | ## Overall Architecture
22 |
23 | The repository actually contains two related but generally separate aspects: the server side and the
24 | client side. They do not share code, but communicate over HTTP. This may seem like an off choice,
25 | but it can prove useful if at some point it becomes required to use an
26 | [isomorphic approach](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) (which can
27 | rather readily be supported).
28 |
29 | The server side is written in Node, and uses [Express][Express]. It is a pretty typical stack,
30 | serving static content out of `public`, using the Express middleware for sessions,
31 | [Winston][Winston] for logging, etc.
32 |
33 | The database system is CouchDB. It is also used in a straightforward manner, with no reliance on
34 | CouchDB specificities. If needed, it could be ported to another system.
35 |
36 | The client side is written using React, making lightweight use of the [Flux][Flux] architecture, and
37 | is built using Browserify. React is its own way of thinking about Web applications that has its own
38 | learning curve (and can require a little bit of retooling of one's editor for the [JSX][JSX] part)
39 | but once you start using it it is hard to go back. It's the first framework I find to be worth the
40 | hype since jQuery (and for completely different reasons).
41 |
42 | No CSS framework is used; but the CSS does get built too using [cleancss][cleancss] (for modularity
43 | and minification).
44 |
45 | ## Setting Up
46 |
47 | Installation is straightforward:
48 |
49 | git clone https://github.com/w3c/ash-nazg
50 | cd ash-nazg
51 | npm install -d
52 |
53 | You now need to configure the system so that it can find various bits and pieces. For this create a
54 | `config.json` at the root, with the following content:
55 |
56 | ```
57 | {
58 | // the root URL, this is what I use on my development machine
59 | "url": "http://ash.bast/"
60 | // the full URL to the GitHub hook; locally I use ngrok to expose that to the world
61 | // (see below for details about ngrok). In production this can be inferred from url+hookPath
62 | , "hookURL": "http://ashnazg.ngrok.io/api/hook"
63 | // the local path for the GitHub hook
64 | , "hookPath": "api/hook"
65 | // pick a port to use
66 | , "serverPort": 3043
67 | // you need a secret to seed the sessions
68 | , "sessionSecret": "Some secret phrase"
69 | // the client ID and secret you get from GitHub
70 | , "ghClientID": "deadbeef"
71 | , "ghClientSecret": "d3adb33f"
72 | // set to true if you want logging to the console (false in production)
73 | , "logToConsole": true
74 | // username and password for Couch
75 | , "couchAuth": {
76 | "username": "robin"
77 | , "password": "some!cool@password"
78 | }
79 | // the database name in Couch
80 | , "couchDB": "ashnazg"
81 | // address from which notifications are set
82 | , "notifyFrom": "foo@example.com"
83 | // w3cbot GitHub token with `public_repo` privileges to comment on the PR
84 | , "w3cBotGHToken": "1234"
85 | }
86 | ```
87 |
88 | Now, with CouchDB is already up and running, you want to run:
89 |
90 | node store.js
91 | node tools/add-admin.js yourGitHubUsername
92 |
93 | This installs all the design documents that Couch needs. Whenever you change the design documents,
94 | just run `store.js` again. You only need to create an admin user on a fresh database; after that
95 | other admins can be minted through the UI.
96 |
97 | To send notifications of failures, ash-nazg assumes sendmail is installed and properly configured on the server.
98 |
99 | Running the server is as simple as:
100 |
101 | npm run start
102 |
103 | If you are going to develop however, that isn't the best way of running the server. If you are
104 | touching several aspects (CSS, client, server) you will want to have several terminals open.
105 |
106 | When developing the server code, you want to run:
107 |
108 | npm run watch-server
109 |
110 | This will start a [nodemon][nodemon] instance that will monitor the changes you make to the *server*
111 | code, and restart it for you.
112 |
113 | When developing client code, you want to run:
114 |
115 | npm run watch
116 |
117 | This will also use nodemon to monitor the CSS and JS/JSX to rebuild them as needed. Be warned that
118 | the JS build can take a second or two, so if nothing changes because you reload too fast that's why.
119 | You can `watch-js` and `watch-css` separately if you want to.
120 |
121 | One of the issues with developing on one's box is that it is not typically accessible over the Web
122 | for outside services to interact with. If you are trying to get events from repositories on GitHub,
123 | you will need to expose yourself to the Web. You may already have your preferred way of doing that,
124 | but in case you don't you can use [ngrok][ngrok] (which is what I do). In order to expose your
125 | service through ngrok, just run
126 |
127 | ```bash
128 | npm run expose # Or, if you don't have an ngrok paid plan:
129 | node_modules/ngrok/bin/ngrok http 3043
130 | ```
131 |
132 | Note that you don't need that for regular development, you only need to be exposed if you want to
133 | receive GitHub events.
134 |
135 | ## Production deployment
136 |
137 | You will want a slightly different `config.json`; the one in hatchery is serviceable.
138 |
139 | You don't want to use `npm run` in production; instead use [pm2][pm2]. A configuration is provided
140 | for it in `pm2-production.json` (it's what's used on hatchery).
141 |
142 | Make sure you create an admin user as described above.
143 |
144 |
145 | ## The CouchDB Design
146 |
147 | A small set of design documents are used in CouchDB, and they are all very simple. They are basic
148 | maps to index the data. You can find them all under `store.js` in `setupDDocs()`. There are:
149 |
150 | * users, that can be queried by username or affiliation;
151 | * groups, queried through their W3C ID or type (WG, etc.);
152 | * secrets (each repository hook has a separate secret so that a rogue repository can be forgotten
153 | about without compromising the others), queried by repository name;
154 | * tokens (that allow us to impersonate users), queried by username;
155 | * repos, queried by name; and
156 | * PRs, queried by any of: repository name and PR number, date, status (open or closed), group that
157 | they below to, or affiliation of contributors.
158 |
159 | ## Server Code Layout
160 |
161 | The server makes use of several files.
162 |
163 | ### `server.js`
164 |
165 | This is the primary entry point, and it does quite a few things. It could be factored out.
166 |
167 | It makes use of Passport and its attendant GitHub login strategy in order to support GitHub logins.
168 | This is basically an OAuth service. When a new user logs in, their user gets created in the DB based
169 | on the information that GitHub provides through Passport.
170 |
171 | There are also Express endpoints for when OAuth completes and we need to handle the actual login at
172 | our end (`/auth/github` and `/auth/github/callback`). The code handles redirections so that the user
173 | should always return to the page that they initially had to log into.
174 |
175 | The server uses long-lived sessions, that are stored as files. This could be replaced with a DB, but
176 | so long as the traffic is reasonable it should not be a problem.
177 |
178 | There is a `logout` endpoint that simply kills the session, and a `logged-in` one that can tell
179 | whether the current user is logged in (and an admin or not).
180 |
181 | Many endpoints simply talk to the store in order to CRUD the data. Nothing fancy.
182 |
183 | The complicated parts are those that handle the interaction with GitHub beyond just the login.
184 |
185 | `makeCreateOrImportRepo()` will drive the `gh` component in order to (yes) create or import a
186 | repository. It will create and store a secret unique to the hook attached to that repo, to make sure
187 | that the secret can leak without enabling people to fake input from any monitored repo. It will also
188 | store the GitHub token that is allowed to manipulate this repo so that we can interact with it even
189 | in the user's absence. Once all works out it adds the repository to the DB.
190 |
191 | The GitHub hooks handling is nasty, sadly because it has to be (see `prStatus()`). This needs to:
192 |
193 | * Find the repository and bail if we're not monitoring it
194 | * Find a token that allows us to set the status of PRs on that repo
195 | * Set the status to pending
196 | * Get existing contributors if the PR is already known about (since it can be updated)
197 | * Look up all the contributors to see if they're allowed to contribute
198 | * Set the status of the PR (and store it) based on the contributors' acceptability
199 |
200 | The handling of the incoming hook is also amusing. Basically, hooks are signed so that we can be
201 | sure they are really coming from GitHub. But since we have a different secret per repo we need to
202 | look inside the payload to figure out which secret to use to validate the signature. Yet we can't
203 | use the normal Express JSON middleware because that will get rid of the incoming bytes, making
204 | signature validation impossible.
205 |
206 | Once we have the repo, the secret, signature validation, and it's the right kind of event we pass
207 | the data on.
208 |
209 | A few endpoints also talk to the `w3capi` library in order to make it easier to use the W3C API.
210 | Nothing fancy.
211 |
212 | Finally, a number of endpoints just map to `showIndex()`. This is there because we use the History
213 | API, which means we can get requests with those paths but they should all just serve the index page.
214 |
215 |
216 | ### `store.js`
217 |
218 | This is a very straightforward access point to CouchDB, built atop the [cradle][cradle] library.
219 | When ran directly it creates the DB and sets up the design documents; otherwise it's a library that
220 | can be used to access the content of the DB.
221 |
222 | Overall it could use some DRY love; a lot of its methods look very much like one another.
223 |
224 | There is no specific handling of conflicts, they should just fail.
225 |
226 | Object types are labelled with a `type` field, and the `id` field is used to know where to store
227 | each object. The `type` field is what the design documents map on.
228 |
229 |
230 | ### `gh.js`
231 |
232 | This library handles most of the interactions with GitHub, on top of the [octokat][octokat] library.
233 | Most of these interactions are simple and linear.
234 |
235 |
236 | ### `log.js`
237 |
238 | This is a simple wrapper that exposes an already-built instance of Winston, configured to log to the
239 | console, file, or both. It's easy to add other logging targets if need be.
240 |
241 |
242 | ## Client Code Layout
243 |
244 | ### `app.css` and `css/fonts.css`
245 |
246 | These are very simple CSS files. They are merged together (along with imported dependencies) and
247 | stored under `public/css`. Therefore that's what their paths are relative to.
248 |
249 | There is no magic and no framework. The complete built CSS is ~5K.
250 |
251 | ### `app.jsx`
252 |
253 | This is the entry point for the JS application. Most of what it does is to import things and get
254 | them set up.
255 |
256 | The whole client JS is written in ES6, JSX, React. This can be surprising at first, but it is a
257 | powerful combo.
258 |
259 | The root `AshNazg` component listens for changes to the login state of the user (through the Login
260 | store) in order to change the navigation bar that it controls. All it renders is basically: the
261 | application title, a simple layout grid (that uses the [ungrid][ungrid] CSS approach), the
262 | navigation bar, and an empty space for the routed component. It also renders the "flash" area that
263 | shows messages for successful operations or errors.
264 |
265 | Finally, the router is set up with a number of paths mapping to imported components.
266 |
267 | ### `components/*.jsx`
268 |
269 | The JSX files under `components/` are simple, reusable components. At some point they should probably be extracted into a shared library that can be reused across W3C applications.
270 |
271 | Most of them are extremely simple and largely there to keep the JSX readable, without having to rely
272 | excessively on `div`s and classes.
273 |
274 | #### `application.jsx`
275 |
276 | A simple layout wrapper, with a title, that just renders its children. Used to render routed
277 | components into.
278 |
279 | #### `col.jsx` and `row.jsx`
280 |
281 | Very simple row and column items that use ungrid. Nothing fancy.
282 |
283 | #### `nav-box.jsx` and `nav-item.jsx`
284 |
285 | Made to be used as a navigation column or as drop down menus, the boxes have titles that label a
286 | navigation section, the items are basically just navigation entries.
287 |
288 | #### `spinner.jsx`
289 |
290 | This is a simple loading/progress spinner (that uses `img/spinner.svg`). If Chrome drops SMIL
291 | support this will need to be replaced by something else. It understands the `prefix` option in order
292 | to still work when the application is not running at the site's root (an improvement would be to
293 | just inline the SVG).
294 |
295 | It also accepts a `size="small"` property which renders it at half size.
296 |
297 | #### `flash-list.jsx`
298 |
299 | This just renders the list of success/error messages that are stored in the message store.
300 |
301 | ### `stores/*.js` and `actions/*.js`
302 |
303 | One architectural approach that works well with React is known as Flux. At its heart it is a simple
304 | idea to handle events and data in an application, in such a manner that avoids tangled-up messes.
305 |
306 | The application (typically driven by the user) can trigger an **action**, usually with attached
307 | data. An example from the code are error messages that can be emitted pretty much anywhere in the
308 | application (ditto success messages).
309 |
310 | Actions are all sent towards the **dispatcher** (which we reuse from the basic Flux implementation).
311 | The dispatcher makes these available to whoever wants to listen. This is similar to pub/sub, except that an event's full trip is taken into consideration, and it only ever travels in one direction.
312 |
313 | Stores listen to actions, and keep any data that the application might need handy (either locally or
314 | by accessing it when needed). For the error/success messages, the store just keeps them around until
315 | they are dismissed, which means that navigation across components will still render the messages in
316 | the store.
317 |
318 | Finally, components can listen to changes in stores, and react to them so as to update thei
319 | rendering.
320 |
321 | Overall, this application should make use of actions and stores a lot more. Developing it further
322 | will likely require refactoring along those lines. One of the great things with React is that the
323 | components are isolated in such a manner that you can follow bad practices inside of a given
324 | component without damaging the rest of the application. Not that this is recommended, but it does
325 | allow one to experiment with what a given component should do before refactoring it. I would not say
326 | that the components in this application follow bad practices, but they could be refactored to use
327 | stores and actions in order to be cleaner and more testable.
328 |
329 | #### `actions/messages.js` and `actions/user.js`
330 |
331 | These are actions. These modules can just be imported by any component that wishes to carry out such
332 | actions, without having to know anything about whether or how the result gets stored, or how it
333 | might influence the rest of the application (it's completely fire-and-forget).
334 |
335 | The `messages.js` action module supports `error()` and `success()` messages, and can `dismiss()` a
336 | given message. The `user.js` action module supports `login()` and `logout()` actions corresponding
337 | to what the user does.
338 |
339 | #### `stores/login.js` and `stores/message.js`
340 |
341 | The `login` store keeps information about whether the user is logged in (and an administrator), and
342 | handles the logging out when requested. The `message` store keeps a list of error and success
343 | messages that haven't been dismissed.
344 |
345 | ### The `application/*.jsx` components
346 |
347 | These are non-reusable components that are specific to this applications.
348 |
349 | #### `welcome.jsx`
350 |
351 | Just a static component with the welcome text; this is only a component because it's the simplest
352 | way of encapsulating anything that may be rendered in the application area.
353 |
354 | #### `login.jsx`
355 |
356 | A very simple component that explains the login process and links to the OAuth processor.
357 |
358 | #### `logout-button.jsx`
359 |
360 | A button that can be used (and reused) anywhere (in our case, it's part of the navigation). When
361 | clicked it dispatches a `logout` action.
362 |
363 | #### `repo-list.jsx`
364 |
365 | A simple component that fetches the list of repositories that are managed and lists them.
366 |
367 | #### `repo-manager.jsx`
368 |
369 | A more elaborate component that handles both creation and importing of repositories into the system.
370 | It handles the dialog for create/import, including listing the organisations that the user has
371 | access to and which groups a repository can be managed by.
372 |
373 | All of the useful repository-management logic is on the server side, but this reacts to the results.
374 |
375 | #### `pr/last-week.jsx`
376 |
377 | The list of pull requests that were processed one way or another during the last week. This
378 | component can also filter them dynamically by affiliation.
379 |
380 | #### `pr/open.jsx`
381 |
382 | The list of currently open PRs.
383 |
384 | #### `pr/view.jsx`
385 |
386 | The detailed view of a single PR, with various affordances to manage it.
387 |
388 | #### `admin/users.jsx` and `admin/user-line.jsx`
389 |
390 | The list of users known to the system, with some details and links to edit them. The `user-line`
391 | component just renders one line in the list of users.
392 |
393 | #### `admin/add-user.jsx`
394 |
395 | A very simple dialog that can be used to add users with.
396 |
397 | #### `admin/edit-user.jsx`
398 |
399 | One of the more intricate parts of the system. Brings in data from GitHub, the W3C API, and the
400 | system in order to bridge together various bits of information about the user, such as the groups
401 | they belong to, their real name, their affiliation, their W3C and GitHub IDs, etc.
402 |
403 | #### `admin/groups.jsx` and `admin/group-line.jsx`
404 |
405 | Lists all the groups known to the W3C API, and makes it possible to add those that are not already
406 | in the system. Each line in the table is rendered by `group-line.jsx`.
407 |
408 | #### `admin/pick-user.jsx`
409 |
410 | A very simple interface that links to `add-user` in order to add a user.
411 |
412 | ## Test suite
413 |
414 | The [test suite](./test/) only deals with the server-side of the app.
415 |
416 | It uses mocha as its test runner, [supertest][supertest] to test the responses from the various routes, and [nock][nock] to mock the third-party APIs the app relies on (Github API, W3C API).
417 |
418 | To run the test suite, you need to have a running instance of couchdb, and initialize it with `node store.js "./test/config-test.json"`; if your couchdb requires a login/password for admin, you should add it to the `config-test.json` file as an entry of the form of:
419 | ```json
420 | "couchAuth": {
421 | "username": "foo"
422 | , "password": "bar"
423 | }
424 | ```
425 |
426 | [CouchDB]: http://couchdb.apache.org/
427 | [Express]: http://expressjs.com/
428 | [Midgard]: https://github.com/w3c/midgard
429 | [React]: https://facebook.github.io/react/docs/getting-started.html
430 | [Flux]: http://facebook.github.io/flux/
431 | [Browserify]: http://browserify.org/
432 | [JSX]: https://facebook.github.io/react/docs/displaying-data.html
433 | [cleancss]: https://github.com/jakubpawlowicz/clean-css
434 | [nodemon]: https://github.com/remy/nodemon
435 | [ngrok]: https://ngrok.com/
436 | [pm2]: https://github.com/Unitech/pm2
437 | [cradle]: https://github.com/flatiron/cradle
438 | [Winston]: http://github.com/flatiron/winston
439 | [ungrid]: http://chrisnager.github.io/ungrid/
440 | [octokat]: https://github.com/philschatz/octokat.js/
441 | [supertest]: https://github.com/visionmedia/supertest
442 | [nock]: https://github.com/node-nock/nock
443 |
--------------------------------------------------------------------------------