feat(tool): Add ability to view all open PRs for repos other than freeCodeCamp in the Dashboard app (#40453)

* feat: show open boilerplate prs on dashboard

fix:  rest of boilerplate server changes

fix: more

fix: other

* fix: update lib functions

* fix: retrofitted one-off scripts

* feat: added rateLimit for requests

* fix: reduce time

* fix: put limiter inside each route

* fix: make client show when rated limited

* fix: removed unused probot from app

* fix: renamed folders

* fix: consolidate config.js and constants.js

* chore: update octokit to latest version

* fix: remove invalid file

* fix: refactored update-db.js

* feat: add fcc logo

* fix: logo url

* fix: remove Home link

* fix: change link colors

* fix: added rate limiter to landing page

* fix: ran npm install in client to create package-lock.json

* fix: correct typo in doc

Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>

* fix: Replace favicon, Gitter => Discord

Signed-off-by: nhcarrigan <nhcarrigan@gmail.com>

* fix: add extra linting guidance to package.json

* Ignore contributor app

Signed-off-by: nhcarrigan <nhcarrigan@gmail.com>

* fix: revert linting rules for client

* fix: add skip_preflight_check=true for tests

Co-authored-by: Kristofer Koishigawa <scissorsneedfoodtoo@gmail.com>

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Kris Koishigawa <scissorsneedfoodtoo@gmail.com>
Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>
This commit is contained in:
Randell Dawson 2020-12-21 21:43:36 -07:00 committed by GitHub
parent 8324a5fcd7
commit c8f6d15688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 12956 additions and 15348 deletions

View File

@ -4,4 +4,4 @@ client/static/**
client/public/**
api-server/public/**
api-server/lib/**
tools/dashboard/**
tools/contributor/**

View File

@ -2,4 +2,4 @@
# Contribute
Tools to help maintain [freeCodeCamp.org](https://www.freecodecamp.org)'s Open Source Codebase on GitHub. We are currently working on creating a tools dashboard for helping you review PRs and translations. Hangout with us in the [contributor's chat room](https://gitter.im/FreeCodeCamp/Contributors) to learn more.
Tools to help maintain [freeCodeCamp.org](https://www.freecodecamp.org)'s Open Source Codebase on GitHub. We are currently working on creating a tools dashboard for helping you review PRs and translations. Hangout with us in the [contributor's room on Discord](https://discord.gg/kgaE5dh) to learn more.

View File

@ -12,12 +12,9 @@
"dev": "develop",
"develop": "cross-env REACT_APP_HOST=local SKIP_PREFLIGHT_CHECK=true react-scripts start",
"build": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build",
"test": "react-scripts test",
"test": "SKIP_PREFLIGHT_CHECK=true react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -1,9 +1,11 @@
import React, { Component } from 'react';
import styled from 'styled-components';
import FreeCodeCampLogo from './assets/freeCodeCampLogo';
import Tabs from './components/Tabs';
import Search from './components/Search';
import Pareto from './components/Pareto';
import Repos from './components/Repos';
import Footer from './components/Footer';
import { ENDPOINT_INFO } from './constants';
@ -48,7 +50,7 @@ const AppNavBar = styled.nav`
}
`;
const imgStyle = { paddingLeft: '30px' };
const logoStyle = { paddingLeft: '30px' };
const titleStyle = { margin: '0', padding: '0' };
@ -76,7 +78,9 @@ class App extends Component {
handleViewChange = ({ target: { id } }) => {
const view = id.replace('tabs-', '');
this.setState(prevState => ({ ...this.clearObj, view }));
this.updateInfo();
if (view === 'reports' || view === 'search') {
this.updateInfo();
}
};
componentDidMount() {
@ -91,18 +95,11 @@ class App extends Component {
return (
<>
<AppNavBar>
<a href="https://freecodecamp.org" target="_blank" rel="noopener noreferrer">
<img
style={imgStyle}
src="https://discourse-user-assets.s3.dualstack.us-east-1.amazonaws.com/original/3X/e/d/ed1c70bda321aaeee9e6c20ab650ce8bc34899fa.svg"
alt="Free Code Camp Logo"
/>
<a href="https://www.freecodecamp.org/" target="_blank" rel="noopener noreferrer" style={logoStyle}>
<FreeCodeCampLogo />
</a>
<h1 style={titleStyle}>Contributor Tools</h1>
<ul className="app-menu">
<li>
<a href="/">Home</a>
</li>
<li>
<a href="https://github.com/freeCodeCamp/freeCodeCamp" target="_blank" rel="noopener noreferrer">GitHub</a>
</li>
@ -114,6 +111,10 @@ class App extends Component {
<Container>
{view === 'search' && <Search />}
{view === 'reports' && <Pareto />}
{view === 'boilerplates' &&
<Repos key='boilerplates' dataFilter={repo => repo._id.includes('boilerplate')} />}
{view === 'other' &&
<Repos key='other' dataFilter={repo => !repo._id.includes('boilerplate') && repo._id !== 'freeCodeCamp'} />}
</Container>
{footerInfo && <Footer footerInfo={footerInfo} />}
</PageContainer>

View File

@ -0,0 +1,114 @@
/* eslint-disable max-len */
import React from 'react';
function freeCodeCampLogo() {
return (
<svg
aria-label='[freeCodeCamp Logo]'
height={24}
role='math'
version='1.1'
viewBox='0 0 210 24'
width={210}
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
>
<defs>
<path
d='m35.42 5.56 0.43 0.05 0.42 0.08 0.39 0.09 0.37 0.12 0.36 0.14 0.32 0.16 0.31 0.18 0.28 0.21 0.27 0.22 0.24 0.24 0.22 0.27 0.2 0.28 0.18 0.31 0.16 0.33 0.13 0.35 0.12 0.37 0.09 0.39 0.08 0.41 0.05 0.44 0.03 0.45 0.01 0.47v0.12 0.11l-0.01 0.1-0.04 0.2-0.04 0.18-0.03 0.08-0.07 0.15-0.04 0.06-0.05 0.06-0.04 0.06-0.06 0.05-0.05 0.05-0.06 0.03-0.06 0.04-0.07 0.03-0.08 0.02-0.07 0.02-0.16 0.02h-8.9v-0.07h-0.02v1.84l0.01 0.24 0.03 0.24 0.03 0.23 0.06 0.22 0.07 0.2 0.09 0.2 0.1 0.17 0.12 0.18 0.13 0.15 0.15 0.16 0.17 0.13 0.18 0.13 0.2 0.11 0.21 0.11 0.23 0.09 0.24 0.09 0.27 0.06 0.27 0.07 0.3 0.05 0.31 0.03 0.32 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.13l0.12-0.01h0.13l0.24-0.02 0.23-0.02 0.11-0.01 0.11-0.02 0.21-0.03 0.1-0.01 0.1-0.02 0.29-0.06 0.09-0.03 0.09-0.02 0.08-0.03 0.09-0.03 0.08-0.03 0.05-0.01 0.15-0.06 0.06-0.03 0.06-0.02 0.12-0.06 0.21-0.11 0.08-0.04 0.07-0.05 0.17-0.09 0.08-0.05 0.09-0.05 0.09-0.06 0.19-0.13 0.1-0.06 0.1-0.07 0.11-0.07 0.12-0.12 0.13-0.1 0.06-0.05 0.05-0.04 0.06-0.04 0.05-0.05 0.09-0.07 0.1-0.06 0.04-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.03-0.01 0.04-0.01 0.02-0.01 0.05-0.01h0.09l0.1 0.01 0.15 0.03 0.04 0.02 0.07 0.04 0.04 0.03 0.09 0.09 0.03 0.04 0.04 0.08 0.01 0.05 0.02 0.05 0.01 0.06 0.02 0.1v0.07 0.07 0.06l-0.01 0.07-0.01 0.06-0.01 0.07-0.06 0.2-0.03 0.06-0.04 0.07-0.03 0.07-0.04 0.07-0.05 0.07-0.05 0.06-0.1 0.14-0.13 0.13-0.13 0.14-0.16 0.14-0.09 0.06-0.08 0.08-0.15 0.1-0.15 0.11-0.32 0.2-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.19 0.08-0.21 0.07-0.42 0.12-0.22 0.05-0.23 0.05-0.47 0.09-0.5 0.06-0.52 0.04-0.27 0.01-0.28 0.01h-0.28-0.48l-0.47-0.03-0.45-0.04-0.42-0.07-0.41-0.07-0.38-0.09-0.36-0.11-0.34-0.13-0.31-0.15-0.3-0.16-0.27-0.18-0.26-0.2-0.22-0.21-0.21-0.23-0.19-0.25-0.16-0.26-0.14-0.29-0.13-0.29-0.1-0.32-0.07-0.34-0.06-0.35-0.03-0.37-0.01-0.39v-4.71l0.01-0.14 0.01-0.12 0.01-0.13 0.04-0.26 0.04-0.13 0.03-0.12 0.08-0.24 0.1-0.24 0.12-0.23 0.06-0.11 0.07-0.12 0.08-0.1 0.08-0.11 0.08-0.1 0.09-0.11 0.19-0.2 0.23-0.23 0.3-0.24 0.15-0.11 0.17-0.11 0.17-0.1 0.17-0.09 0.18-0.09 0.38-0.16 0.2-0.07 0.42-0.12 0.22-0.05 0.23-0.05 0.22-0.04 0.24-0.03 0.24-0.04 0.24-0.02 0.25-0.02 0.52-0.02h0.27l0.47 0.01 0.47 0.03zm-2.04 1.6-0.41 0.07-0.39 0.1-0.38 0.13-0.37 0.15-0.35 0.18-0.32 0.25-0.27 0.26-0.23 0.28-0.19 0.3-0.13 0.31-0.08 0.32-0.03 0.35v0.96h8.19l-0.09-0.98-0.25-0.83-0.43-0.69-0.6-0.53-0.76-0.38-0.95-0.23-1.11-0.07-0.43 0.01-0.42 0.04z'
id='k'
/>
<path
d='m107.21 5.56 0.43 0.05 0.42 0.08 0.39 0.09 0.37 0.12 0.35 0.14 0.33 0.16 0.31 0.18 0.29 0.21 0.26 0.22 0.24 0.24 0.22 0.27 0.21 0.28 0.17 0.31 0.16 0.33 0.14 0.35 0.11 0.37 0.1 0.39 0.07 0.41 0.05 0.44 0.03 0.45 0.01 0.47v0.12l-0.01 0.11-0.02 0.2-0.02 0.1-0.02 0.09-0.03 0.09-0.02 0.08-0.03 0.07-0.04 0.08-0.04 0.06-0.1 0.12-0.1 0.1-0.13 0.07-0.13 0.05-0.08 0.02-0.16 0.02-8.92 0.01v1.76l0.01 0.24 0.02 0.24 0.04 0.23 0.06 0.22 0.07 0.2 0.08 0.2 0.11 0.17 0.11 0.18 0.14 0.15 0.15 0.16 0.17 0.13 0.18 0.13 0.19 0.11 0.22 0.11 0.23 0.09 0.24 0.09 0.26 0.06 0.28 0.07 0.3 0.05 0.31 0.03 0.32 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.13l0.25-0.01 0.24-0.02 0.22-0.02 0.12-0.01 0.11-0.02 0.31-0.04 0.2-0.04 0.19-0.04 0.09-0.03 0.09-0.02 0.09-0.03 0.08-0.03 0.13-0.04 0.04-0.02 0.06-0.02 0.05-0.02 0.06-0.03 0.05-0.02 0.07-0.03 0.2-0.1 0.14-0.08 0.08-0.05 0.08-0.04 0.08-0.05 0.18-0.1 0.18-0.12 0.2-0.13 0.1-0.07 0.11-0.07 0.06-0.06 0.07-0.06 0.05-0.05 0.07-0.05 0.05-0.05 0.06-0.04 0.11-0.09 0.05-0.03 0.05-0.04 0.04-0.03 0.05-0.03 0.04-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.12-0.04h0.02 0.09 0.05l0.06 0.01 0.09 0.02 0.05 0.01 0.12 0.06 0.12 0.12 0.03 0.04 0.04 0.08 0.01 0.05 0.02 0.05 0.02 0.11 0.01 0.05v0.07 0.13l-0.01 0.07-0.01 0.06-0.01 0.07-0.04 0.14-0.02 0.06-0.03 0.06-0.04 0.07-0.03 0.07-0.09 0.14-0.05 0.06-0.05 0.07-0.11 0.14-0.21 0.2-0.23 0.2-0.09 0.08-0.3 0.21-0.15 0.1-0.17 0.1-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.2 0.08-0.2 0.07-0.42 0.12-0.22 0.05-0.23 0.05-0.23 0.04-0.24 0.05-0.5 0.06-0.52 0.04-0.55 0.02h-0.28-0.49l-0.46-0.03-0.45-0.04-0.43-0.07-0.39-0.07-0.39-0.09-0.36-0.11-0.33-0.13-0.33-0.15-0.29-0.16-0.27-0.18-0.25-0.2-0.23-0.21-0.22-0.23-0.18-0.25-0.17-0.26-0.14-0.29-0.12-0.29-0.1-0.32-0.08-0.34-0.05-0.35-0.04-0.37-0.01-0.39v-4.58l0.01-0.13v-0.14l0.02-0.12 0.01-0.13 0.02-0.13 0.05-0.26 0.12-0.36 0.1-0.24 0.11-0.23 0.07-0.11 0.07-0.12 0.07-0.1 0.09-0.11 0.08-0.1 0.09-0.11 0.19-0.2 0.1-0.1 0.14-0.13 0.14-0.12 0.15-0.12 0.15-0.11 0.17-0.11 0.17-0.1 0.17-0.09 0.19-0.09 0.18-0.08 0.19-0.08 0.2-0.07 0.42-0.12 0.44-0.1 0.23-0.04 0.23-0.03 0.25-0.04 0.24-0.02 0.25-0.02 0.52-0.02h0.27l0.48 0.01 0.46 0.03zm-2.04 1.6-0.41 0.07-0.39 0.1-0.38 0.13-0.37 0.15-0.34 0.18-0.34 0.25-0.29 0.26-0.23 0.28-0.17 0.3-0.13 0.31-0.07 0.32-0.03 0.35v0.96h8.19l-0.09-0.98-0.25-0.83-0.43-0.69-0.6-0.53-0.76-0.38-0.95-0.23-1.11-0.07-0.43 0.01-0.42 0.04z'
id='j'
/>
<path
d='m203.57 0.17c-0.12 0.12-0.24 0.29-0.24 0.45 0 0.29 0.34 0.69 0.97 1.33 2.63 2.53 3.95 5.62 3.94 9.35-0.01 4.13-1.4 7.45-4.1 10.01-0.57 0.51-0.8 0.91-0.8 1.25 0 0.17 0.12 0.34 0.23 0.51 0.11 0.12 0.34 0.23 0.51 0.23 0.62 0 1.5-0.73 2.64-2.17 2.22-2.72 3.22-5.73 3.28-9.82 0.05-4.1-1.23-6.88-3.75-9.75-0.9-1.03-1.66-1.56-2.17-1.56-0.17 0-0.35 0.06-0.51 0.17z'
id='b'
/>
<path
d='m124.75 1.76c1.14 0.86 1.73 2.07 1.73 3.55 0 0.68-0.29 1.02-0.86 1.02-0.39 0-0.68-0.34-0.85-1.02-0.11-0.57-0.34-1.08-0.62-1.62-0.52-0.9-1.61-1.32-3.32-1.32-1.49 0-2.52 0.34-3.14 1.08-0.57 0.68-0.91 1.72-0.91 3.26v5.95c0 1.55 0.34 2.63 0.97 3.31 0.68 0.74 1.72 1.13 3.2 1.13 2.23 0 3.54-0.79 3.82-2.34 0.12-0.57 0.17-0.86 0.17-0.91 0.12-0.34 0.35-0.51 0.68-0.51 0.57 0 0.86 0.34 0.86 1.02 0 1.44-0.57 2.52-1.78 3.38-0.97 0.62-2.18 0.96-3.77 0.96-1.84 0-3.26-0.4-4.3-1.25-1.16-0.8-1.73-2.16-1.73-3.94v-7.16c0-3.77 1.95-5.61 5.95-5.61 1.61 0 2.86 0.34 3.9 1.02z'
id='n'
/>
<path
d='m14.21 6.57c0-0.56 0.34-0.79 1.02-0.79h3.32c0.57 0 0.85 0.51 0.85 1.44 1.02-1.08 2.12-1.73 3.26-1.73 0.96 0 1.72 0.29 2.23 0.86 0.57 0.57 0.8 1.38 0.8 2.29 0 0.63-0.29 0.97-0.8 0.97-0.34 0-0.57-0.23-0.68-0.63-0.23-0.8-0.34-1.19-0.4-1.25-0.22-0.39-0.68-0.62-1.25-0.62-0.62 0-1.25 0.23-1.78 0.68-0.34 0.23-0.8 0.74-1.38 1.49v7.67h3.08c0.68 0 1.03 0.29 1.03 0.8 0 0.57-0.35 0.86-1.03 0.86h-7.33c-0.68 0-1.02-0.29-1.02-0.8 0-0.57 0.34-0.8 1.02-0.8h2.52v-0.07h0.02v-9.57h-2.46c-0.68 0-1.02-0.28-1.02-0.8z'
id='l'
/>
<path
d='m96.68 0.04 0.06 0.02 0.06 0.03 0.05 0.03 0.06 0.04 0.13 0.13 0.03 0.06 0.04 0.06 0.03 0.07 0.02 0.07 0.04 0.16 0.04 0.18 0.01 0.2v0.1 16.84 0.08l-0.01 0.07v0.07l-0.04 0.13-0.01 0.05-0.03 0.06-0.02 0.05-0.06 0.09-0.07 0.08-0.05 0.03-0.04 0.03-0.05 0.03-0.1 0.04-0.06 0.02-0.12 0.02-0.07 0.01h-0.11l-0.13-0.02-0.04-0.01-0.04-0.02-0.04-0.01-0.03-0.02-0.03-0.01-0.03-0.03-0.03-0.02-0.06-0.05-0.02-0.03-0.07-0.12-0.01-0.04-0.02-0.05-0.01-0.04-0.02-0.05v-0.05-0.08-0.04-0.04l-0.01-0.04v-0.04l-0.01-0.04v-0.04-0.04l-0.01-0.05v-0.04-0.05l-0.01-0.09v-0.06l-0.01-0.04v-0.06-0.11l-0.01-0.06v-0.13l-0.13 0.09-0.13 0.08-0.13 0.09-0.24 0.14-0.11 0.08-0.12 0.07-0.21 0.12-0.1 0.06-0.09 0.06-0.1 0.05-0.25 0.14-0.14 0.08-0.14 0.06-0.12 0.05-0.05 0.02-0.09 0.04-0.09 0.03-0.09 0.02-0.19 0.06-0.4 0.08-0.21 0.03-0.22 0.03-0.11 0.02-0.23 0.02h-0.12l-0.12 0.01h-0.11l-0.12 0.01h-0.46l-0.21-0.01-0.2-0.01-0.2-0.02-0.19-0.01-0.2-0.03-0.18-0.02-0.37-0.07-0.34-0.08-0.18-0.05-0.32-0.1-0.32-0.12-0.15-0.07-0.15-0.06-0.42-0.24-0.13-0.08-0.14-0.1-0.28-0.22-0.12-0.11-0.12-0.13-0.11-0.12-0.11-0.13-0.1-0.14-0.09-0.14-0.17-0.29-0.07-0.15-0.07-0.16-0.06-0.16-0.05-0.16-0.05-0.18-0.04-0.18-0.03-0.17-0.02-0.19-0.03-0.19-0.01-0.19v-5.21l0.01-0.19 0.03-0.18 0.02-0.18 0.03-0.18 0.08-0.34 0.05-0.16 0.06-0.16 0.06-0.15 0.08-0.15 0.07-0.15 0.18-0.28 0.2-0.26 0.1-0.12 0.24-0.24 0.26-0.22 0.14-0.1 0.39-0.24 0.15-0.07 0.28-0.14 0.31-0.12 0.15-0.05 0.33-0.1 0.33-0.08 0.72-0.12 0.37-0.03 0.38-0.02h0.45l0.25 0.02h0.12 0.13l0.12 0.02 0.12 0.01 0.22 0.02 0.22 0.04 0.11 0.01 0.21 0.04 0.1 0.02 0.09 0.02 0.28 0.08 0.09 0.03 0.09 0.04 0.08 0.03 0.09 0.03 0.08 0.04 0.09 0.03 0.09 0.04 0.1 0.05 0.17 0.09 0.29 0.16 0.09 0.05 0.29 0.19 0.2 0.14 0.09 0.07 0.4 0.32v-5.89l0.01-0.1v-0.2l0.04-0.18 0.01-0.08 0.03-0.08 0.02-0.07 0.03-0.07 0.03-0.06 0.04-0.06 0.04-0.04 0.04-0.05 0.04-0.04 0.05-0.04 0.1-0.06 0.06-0.02 0.13-0.03 0.06-0.01h0.14l0.07 0.01 0.14 0.03zm-5.7 7.19-0.26 0.03-0.26 0.02-0.25 0.05-0.24 0.05-0.23 0.05-0.42 0.16-0.19 0.09-0.18 0.1-0.2 0.15-0.2 0.16-0.17 0.17-0.15 0.17-0.14 0.19-0.11 0.2-0.1 0.2-0.08 0.23-0.05 0.23-0.04 0.23-0.01 0.26v4.52l0.01 0.26 0.03 0.25 0.05 0.23 0.07 0.23 0.09 0.21 0.12 0.2 0.13 0.19 0.16 0.17 0.17 0.16 0.2 0.14 0.22 0.14 0.19 0.09 0.42 0.16 0.22 0.07 0.22 0.06 0.24 0.06 0.25 0.04 0.26 0.04 0.27 0.03 0.56 0.02 0.47-0.01 0.44-0.04 0.43-0.06 0.21-0.04 0.2-0.06 0.38-0.12 0.17-0.07 0.14-0.1 0.16-0.11 0.15-0.11 0.16-0.12 0.17-0.12 0.16-0.13 0.18-0.13 0.17-0.15 0.18-0.15 0.36-0.32v-6.59l-0.21-0.16-0.21-0.14-0.2-0.14-0.2-0.13-0.19-0.12-0.18-0.11-0.17-0.11-0.16-0.09-0.3-0.14-0.13-0.06-0.19-0.07-0.38-0.12-0.2-0.05-0.4-0.08-0.21-0.03-0.21-0.02-0.21-0.01-0.43-0.01h-0.28l-0.27 0.01z'
id='c'
/>
<path
d='m195.66 12.04c-0.99-0.25 3.06-5.03-4.13-10.75 0 0 0.94 3-3.81 9.69-4.76 6.68 2.11 10.66 2.11 10.66s-3.22-1.72 0.53-7.84c0.67-1.11 1.55-2.11 2.64-4.38 0 0 0.96 1.37 0.46 4.32-0.75 4.47 3.27 3.19 3.33 3.25 1.41 1.65-1.16 4.56-1.32 4.65s7.34-4.5 2.01-11.42c-0.36 0.36-0.83 2.08-1.82 1.82z'
id='e'
/>
<path
d='m135.26 5.37 0.19 0.01 0.18 0.02 0.18 0.01 0.18 0.02 0.34 0.04 0.16 0.02 0.16 0.04 0.15 0.02 0.14 0.04 0.15 0.03 0.28 0.08 0.26 0.08 0.36 0.15 0.12 0.06 0.11 0.05 0.1 0.06 0.11 0.06 0.12 0.08 0.1 0.09 0.11 0.09 0.2 0.2 0.18 0.22 0.16 0.24 0.07 0.12 0.14 0.26 0.12 0.29 0.05 0.14 0.04 0.15 0.05 0.16 0.04 0.15 0.03 0.17 0.02 0.16 0.04 0.36 0.01 0.18 0.02 0.18v6.1 0.08 0.08l0.02 0.28 0.01 0.07 0.02 0.12 0.02 0.06 0.01 0.06 0.03 0.11 0.02 0.05 0.06 0.14 0.03 0.04 0.02 0.04 0.03 0.04 0.06 0.07 0.08 0.08 0.02 0.01v0.01h0.03l0.01 0.02 0.05 0.02 0.02 0.01 0.05 0.02 0.03 0.02 0.04 0.01 0.11 0.04 0.12 0.05 0.1 0.04 0.04 0.01 0.05 0.02 0.05 0.03 0.09 0.03 0.07 0.03 0.02 0.02 0.03 0.02 0.05 0.04 0.02 0.02 0.02 0.03 0.02 0.02 0.04 0.06 0.03 0.06 0.01 0.04 0.01 0.03 0.02 0.08v0.04l0.01 0.04 0.01 0.05v0.05l0.01 0.05v0.21l-0.02 0.05v0.04l-0.01 0.05-0.01 0.04-0.02 0.04-0.01 0.04-0.02 0.03-0.03 0.04-0.05 0.06-0.02 0.03-0.04 0.02-0.11 0.06-0.08 0.04-0.1 0.02-0.1 0.01h-0.06-0.11-0.02-0.01l-0.03-0.01h-0.02-0.02l-0.1-0.02-0.03-0.02h-0.03l-0.06-0.02-0.11-0.03-0.12-0.03-0.05-0.01-0.04-0.01-0.05-0.02-0.21-0.07-0.07-0.02-0.07-0.03-0.24-0.08-0.1-0.04-0.05-0.01-0.04-0.02-0.05-0.01-0.04-0.01-0.04-0.02-0.03-0.01-0.07-0.02-0.07-0.03-0.03-0.01-0.01-0.01-0.02-0.01h-0.03-0.04l-0.03-0.02-0.03-0.01-0.04-0.01-0.02-0.02-0.06-0.04-0.03-0.03-0.02-0.03-0.03-0.03-0.02-0.03-0.02-0.04-0.03-0.04-0.02-0.04-0.01-0.04-0.03-0.04-0.02-0.05-0.03-0.09-0.06-0.21-0.02-0.06-0.23 0.16-0.22 0.15-0.1 0.07-0.2 0.12-0.1 0.07-0.34 0.22-0.08 0.04-0.07 0.05-0.21 0.12-0.06 0.04-0.16 0.08-0.05 0.02-0.09 0.04-0.04 0.01-0.06 0.04-0.07 0.02-0.15 0.05-0.4 0.1-0.08 0.01-0.18 0.03-0.09 0.01-0.09 0.02-0.28 0.03-0.2 0.02h-0.1l-0.11 0.01h-0.1-0.49-0.16l-0.16-0.01-0.31-0.02-0.3-0.03-0.15-0.02-0.15-0.03-0.28-0.05-0.27-0.06-0.14-0.04-0.13-0.04-0.12-0.04-0.13-0.05-0.24-0.09-0.12-0.06-0.11-0.05-0.11-0.06-0.11-0.07-0.11-0.06-0.1-0.07-0.24-0.16-0.22-0.19-0.1-0.1-0.09-0.1-0.1-0.1-0.24-0.33-0.14-0.24-0.06-0.12-0.06-0.13-0.05-0.12-0.05-0.13-0.08-0.27-0.06-0.28-0.04-0.3-0.02-0.3v-0.32l0.01-0.15 0.01-0.16 0.02-0.14 0.05-0.3 0.07-0.28 0.05-0.14 0.11-0.26 0.12-0.26 0.15-0.24 0.16-0.22 0.09-0.11 0.1-0.11 0.2-0.2 0.11-0.09 0.23-0.18 0.13-0.08 0.33-0.24 0.24-0.13 0.26-0.13 0.13-0.05 0.26-0.11 0.14-0.05 0.42-0.12 0.15-0.03 0.14-0.04 0.15-0.02 0.15-0.03 0.16-0.02 0.15-0.01 0.16-0.02 0.48-0.03h0.3l0.13 0.01h0.14l0.29 0.02 0.14 0.02 0.15 0.02 0.14 0.02 0.16 0.02 0.14 0.02 0.15 0.04 0.16 0.02 0.15 0.03 0.32 0.08 0.49 0.12 0.16 0.05 0.51 0.15 0.36 0.12 0.17 0.06v-2.01-0.12l-0.01-0.11-0.01-0.1-0.01-0.12-0.02-0.09-0.02-0.11-0.04-0.1-0.03-0.1-0.03-0.09-0.05-0.09-0.04-0.09-0.11-0.16-0.06-0.09-0.06-0.07-0.07-0.08-0.15-0.14-0.08-0.07-0.18-0.12-0.1-0.06-0.2-0.1-0.11-0.06-0.07-0.03-0.16-0.06-0.34-0.11-0.26-0.06-0.1-0.02-0.09-0.01-0.1-0.02-0.09-0.01-0.3-0.03-0.1-0.02h-0.1l-0.11-0.01-0.1-0.01h-0.11-0.1-0.47l-0.25 0.01-0.24 0.01-0.23 0.02-0.42 0.06-0.2 0.03-0.19 0.04-0.17 0.05-0.17 0.06-0.16 0.05-0.15 0.07-0.14 0.07-0.13 0.07-0.12 0.09-0.12 0.08-0.1 0.1-0.09 0.1-0.09 0.11-0.08 0.11-0.06 0.11-0.06 0.13-0.05 0.13-0.04 0.14-0.03 0.14-0.01 0.07-0.02 0.06-0.04 0.22-0.02 0.09-0.01 0.05-0.01 0.04-0.01 0.03v0.04l-0.02 0.06-0.01 0.06-0.01 0.04v0.02l-0.01 0.02v0.01 0.01 0.03l-0.02 0.03-0.03 0.07-0.01 0.02-0.05 0.06-0.01 0.02-0.03 0.02-0.05 0.05-0.02 0.01-0.02 0.02-0.03 0.01-0.03 0.02-0.09 0.03-0.04 0.01-0.06 0.02h-0.04-0.04l-0.03 0.01h-0.15l-0.06-0.01-0.13-0.02-0.06-0.01-0.15-0.06-0.1-0.06-0.04-0.04-0.04-0.03-0.08-0.08-0.03-0.05-0.05-0.09-0.03-0.06-0.01-0.06-0.04-0.12-0.01-0.06v-0.07l-0.01-0.08v-0.17l0.01-0.1v-0.1l0.02-0.1 0.01-0.1 0.04-0.2 0.05-0.2 0.03-0.1 0.03-0.09 0.08-0.2 0.05-0.1 0.09-0.19 0.18-0.28 0.06-0.09 0.07-0.09 0.22-0.27 0.08-0.09 0.1-0.11 0.22-0.2 0.12-0.09 0.26-0.18 0.14-0.08 0.15-0.08 0.16-0.08 0.15-0.06 0.17-0.07 0.18-0.06 0.18-0.05 0.38-0.1 0.2-0.04 0.42-0.08 0.44-0.05 0.46-0.04 0.24-0.01h0.25l0.25-0.01h0.2l0.2 0.01h0.2zm-2.41 7.31-0.68 0.24-0.54 0.35-0.38 0.44-0.23 0.54-0.07 0.64 0.02 0.34 0.06 0.31 0.11 0.3 0.15 0.27 0.2 0.25 0.25 0.2 0.29 0.17 0.25 0.13 0.28 0.12 0.3 0.09 0.32 0.08 0.34 0.05 0.36 0.03 0.38 0.01 0.34-0.01 0.32-0.02 0.32-0.04 0.31-0.07 0.3-0.07 0.3-0.11 0.29-0.13 0.2-0.12 0.22-0.15 0.25-0.16 0.26-0.18 0.28-0.21 0.3-0.22 0.31-0.23v-2.41l-0.54-0.16-0.53-0.14-0.53-0.11-0.52-0.09-0.51-0.07-0.49-0.04-0.48-0.01-0.98 0.05-0.83 0.14z'
id='m'
/>
<path
d='m0.97 5.8h1.84v-1.61c0-2.8 1.44-4.19 4.24-4.19 1.14 0 2.12 0.23 2.86 0.63 0.96 0.57 1.5 1.5 1.5 2.58 0 0.73-0.29 1.02-0.8 1.02-0.34 0-0.68-0.23-0.86-0.63-0.22-0.73-0.45-1.13-0.56-1.32-0.34-0.4-1.03-0.63-2.01-0.63-1.72 0-2.57 0.85-2.57 2.58v1.55h3.31c0.74 0 1.08 0.29 1.08 0.79 0 0.57-0.34 0.8-1.08 0.8h-3.31v10.48c0 0.62-0.29 0.96-0.8 0.96-0.57 0-0.8-0.34-0.8-0.96v-10.46h-2.04c-0.63 0-0.97-0.28-0.97-0.79 0-0.58 0.34-0.8 0.97-0.8z'
id='a'
/>
<path
d='m78.8 5.55 0.61 0.08 0.56 0.12 0.52 0.15 0.48 0.18 0.43 0.22 0.38 0.25 0.34 0.29 0.29 0.32 0.25 0.35 0.21 0.39 0.16 0.42 0.11 0.45 0.07 0.49 0.02 0.51v4.71l-0.02 0.52-0.07 0.48-0.11 0.46-0.15 0.42-0.2 0.38-0.24 0.35-0.29 0.32-0.33 0.29-0.38 0.25-0.42 0.22-0.47 0.19-0.51 0.15-0.55 0.11-0.6 0.09-0.64 0.05-0.68 0.02-0.72-0.01-0.68-0.04-0.63-0.08-0.58-0.11-0.53-0.15-0.48-0.18-0.43-0.22-0.39-0.25-0.34-0.29-0.3-0.32-0.25-0.36-0.21-0.38-0.15-0.43-0.11-0.46-0.07-0.49-0.02-0.53v-4.71l0.02-0.51 0.07-0.49 0.11-0.45 0.16-0.42 0.2-0.39 0.26-0.35 0.29-0.32 0.34-0.29 0.39-0.25 0.43-0.22 0.47-0.18 0.52-0.15 0.56-0.12 0.61-0.08 0.65-0.06 0.7-0.01 0.69 0.01 0.65 0.06zm-2.67 1.61-0.53 0.1-0.47 0.13-0.42 0.17-0.37 0.21-0.31 0.24-0.25 0.28-0.2 0.32-0.14 0.35-0.09 0.39-0.02 0.42v4.71l0.02 0.42 0.09 0.39 0.14 0.36 0.21 0.31 0.26 0.28 0.31 0.24 0.37 0.2 0.43 0.17 0.49 0.12 0.55 0.09 0.6 0.04h0.66 0.64l0.59-0.04 0.54-0.08 0.48-0.12 0.42-0.16 0.37-0.2 0.31-0.23 0.26-0.28 0.2-0.32 0.14-0.36 0.09-0.4 0.03-0.43v-4.71l-0.03-0.42-0.09-0.39-0.14-0.35-0.2-0.32-0.27-0.28-0.31-0.24-0.38-0.21-0.44-0.17-0.49-0.13-0.55-0.1-0.62-0.05-0.67-0.02-0.63 0.02-0.58 0.05z'
id='i'
/>
<path
d='m181.88 0.18c0.12 0.11 0.23 0.28 0.23 0.45 0 0.29-0.34 0.68-0.97 1.32-2.62 2.53-3.94 5.62-3.93 9.36 0.01 4.12 1.4 7.44 4.1 10.01 0.56 0.5 0.8 0.9 0.8 1.24 0 0.17-0.12 0.35-0.23 0.51-0.11 0.12-0.34 0.24-0.51 0.24-0.63 0-1.5-0.74-2.64-2.18-2.22-2.72-3.22-5.72-3.28-9.82-0.05-4.1 1.23-6.88 3.75-9.75 0.9-1.02 1.66-1.56 2.17-1.56 0.17 0 0.34 0.06 0.51 0.18z'
id='f'
/>
<path
d='m149.59 6.94c0.45-0.57 0.85-0.92 1.25-1.08 0.39-0.23 0.96-0.34 1.6-0.34 1.96 0 2.98 0.96 2.98 2.85v9.29c0 0.79-0.28 1.14-0.85 1.14s-0.8-0.35-0.8-1.14v-8.7c0-1.19-0.51-1.83-1.49-1.83-0.74 0-1.5 0.45-2.12 1.32v9.29c0 0.79-0.29 1.13-0.8 1.13s-0.8-0.34-0.8-1.13v-8.59c0-1.33-0.56-1.95-1.61-1.95-0.68 0-1.32 0.46-2 1.33v9.22c0 0.8-0.29 1.14-0.86 1.14s-0.79-0.34-0.79-1.14v-11.38c0-0.57 0.22-0.8 0.68-0.8 0.23 0 0.45 0.17 0.57 0.51 0.11 0.15 0.17 0.44 0.17 0.78 0.53-0.57 0.87-0.91 1.02-1.03 0.34-0.22 0.8-0.34 1.44-0.34 0.91 0 1.72 0.46 2.41 1.45z'
id='g'
/>
<path
d='m49.79 5.56 0.44 0.05 0.41 0.08 0.4 0.09 0.37 0.12 0.35 0.14 0.33 0.16 0.31 0.18 0.28 0.21 0.27 0.22 0.24 0.24 0.22 0.27 0.2 0.28 0.18 0.31 0.16 0.33 0.13 0.35 0.11 0.37 0.1 0.39 0.07 0.41 0.06 0.44 0.03 0.45 0.01 0.47v0.12l-0.01 0.11-0.01 0.1-0.03 0.2-0.02 0.09-0.03 0.09-0.02 0.08-0.08 0.15-0.03 0.06-0.1 0.12-0.05 0.05-0.06 0.05-0.05 0.03-0.07 0.04-0.07 0.03-0.14 0.04-0.08 0.01-0.09 0.01h-8.89v-0.07h-0.02v1.84l0.01 0.24 0.02 0.24 0.04 0.23 0.06 0.22 0.06 0.2 0.09 0.2 0.1 0.17 0.12 0.18 0.14 0.15 0.15 0.16 0.16 0.13 0.18 0.13 0.2 0.11 0.22 0.11 0.22 0.09 0.25 0.09 0.26 0.06 0.28 0.07 0.29 0.05 0.31 0.03 0.33 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.12l0.13-0.01h0.12l0.24-0.02 0.23-0.02 0.11-0.01 0.12-0.02 0.31-0.04 0.1-0.02 0.09-0.02 0.1-0.02 0.09-0.02 0.1-0.03 0.09-0.02 0.16-0.06 0.09-0.03 0.04-0.01 0.05-0.02 0.06-0.02 0.05-0.02 0.05-0.03 0.06-0.02 0.06-0.03 0.07-0.03 0.14-0.07 0.14-0.08 0.08-0.05 0.08-0.04 0.08-0.05 0.18-0.1 0.08-0.06 0.19-0.13 0.1-0.06 0.11-0.07 0.1-0.07 0.13-0.12 0.13-0.1 0.05-0.05 0.06-0.04 0.05-0.04 0.06-0.05 0.09-0.07 0.09-0.06 0.05-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.09-0.03 0.05-0.01h0.09l0.1 0.01 0.15 0.03 0.04 0.02 0.07 0.04 0.04 0.03 0.09 0.09 0.03 0.04 0.04 0.08 0.02 0.1 0.02 0.06v0.05l0.01 0.05v0.07l0.01 0.07-0.01 0.06v0.07l-0.01 0.06-0.01 0.07-0.07 0.2-0.03 0.06-0.06 0.14-0.09 0.14-0.05 0.06-0.11 0.14-0.12 0.13-0.14 0.14-0.16 0.14-0.08 0.06-0.08 0.08-0.15 0.1-0.15 0.11-0.32 0.2-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.2 0.08-0.2 0.07-0.21 0.06-0.22 0.06-0.44 0.1-0.47 0.09-0.5 0.06-0.52 0.04-0.55 0.02h-0.28-0.49l-0.47-0.03-0.44-0.04-0.43-0.07-0.4-0.07-0.38-0.09-0.36-0.11-0.34-0.13-0.32-0.15-0.29-0.16-0.27-0.18-0.26-0.2-0.23-0.21-0.21-0.23-0.19-0.25-0.16-0.26-0.14-0.29-0.12-0.29-0.1-0.32-0.08-0.34-0.06-0.35-0.03-0.37-0.01-0.39v-4.71l0.01-0.14 0.01-0.12 0.06-0.39 0.03-0.13 0.03-0.12 0.05-0.12 0.08-0.24 0.06-0.12 0.11-0.23 0.07-0.11 0.07-0.12 0.07-0.1 0.08-0.11 0.09-0.1 0.09-0.11 0.18-0.2 0.1-0.1 0.14-0.13 0.29-0.24 0.32-0.22 0.17-0.1 0.36-0.18 0.38-0.16 0.2-0.07 0.42-0.12 0.44-0.1 0.23-0.04 0.23-0.03 0.24-0.04 0.24-0.02 0.26-0.02 0.52-0.02h0.26l0.48 0.01 0.46 0.03zm-2.04 1.6-0.4 0.07-0.4 0.1-0.38 0.13-0.36 0.15-0.35 0.18-0.34 0.25-0.28 0.26-0.23 0.28-0.17 0.3-0.13 0.31-0.08 0.32-0.02 0.35v0.96h8.18l-0.08-0.98-0.26-0.83-0.42-0.69-0.6-0.53-0.77-0.38-0.94-0.23-1.11-0.07-0.44 0.01-0.42 0.04z'
id='h'
/>
<path
d='m67.34 1.76c1.14 0.86 1.73 2.07 1.73 3.55 0 0.68-0.29 1.02-0.86 1.02-0.4 0-0.68-0.34-0.85-1.02-0.12-0.57-0.34-1.08-0.62-1.62-0.52-0.9-1.61-1.32-3.32-1.32-1.5 0-2.52 0.34-3.14 1.08-0.57 0.68-0.92 1.72-0.92 3.26v5.95c0 1.55 0.35 2.63 0.97 3.31 0.68 0.74 1.73 1.13 3.21 1.13 2.23 0 3.54-0.79 3.82-2.34 0.11-0.57 0.17-0.86 0.17-0.91 0.11-0.34 0.34-0.51 0.68-0.51 0.57 0 0.86 0.34 0.86 1.02 0 1.44-0.58 2.52-1.79 3.38-0.96 0.62-2.18 0.96-3.77 0.96-1.83 0-3.25-0.4-4.3-1.25-1.21-0.8-1.72-2.16-1.72-3.94v-7.16c0-3.77 1.93-5.61 5.95-5.61 1.61 0 2.86 0.34 3.9 1.02z'
id='o'
/>
<path
d='m158.79 5.43 0.12 0.04 0.03 0.02 0.03 0.01 0.03 0.03 0.06 0.04 0.03 0.03 0.03 0.02 0.1 0.14 0.06 0.08 0.02 0.05 0.02 0.04 0.01 0.04 0.02 0.09 0.01 0.04 0.02 0.04 0.01 0.05 0.01 0.04 0.02 0.04 0.01 0.05 0.05 0.12 0.02 0.05 0.03 0.08 0.01 0.05 0.03 0.12 0.02 0.04 0.02 0.08 0.25-0.12 0.49-0.23 0.24-0.11 0.23-0.1 0.46-0.18 0.22-0.08 0.21-0.08 0.21-0.07 0.2-0.06 0.2-0.07 0.2-0.05 0.38-0.09 0.19-0.04 0.34-0.06 0.17-0.02 0.32-0.02h0.16l0.46 0.01 0.44 0.03 0.43 0.04 0.39 0.06 0.38 0.09 0.36 0.1 0.33 0.12 0.31 0.14 0.29 0.15 0.27 0.17 0.25 0.2 0.22 0.2 0.21 0.23 0.18 0.24 0.16 0.26 0.14 0.28 0.12 0.3 0.09 0.31 0.08 0.33 0.06 0.35 0.03 0.36 0.01 0.38v4.76l-0.01 0.19-0.01 0.18-0.01 0.17-0.03 0.18-0.02 0.17-0.08 0.32-0.05 0.16-0.05 0.15-0.12 0.3-0.08 0.14-0.07 0.14-0.09 0.14-0.09 0.13-0.09 0.12-0.1 0.13-0.11 0.12-0.12 0.12-0.11 0.12-0.26 0.22-0.24 0.18-0.26 0.16-0.14 0.07-0.13 0.07-0.15 0.07-0.15 0.06-0.15 0.05-0.32 0.11-0.33 0.08-0.17 0.04-0.36 0.06-0.36 0.04-0.19 0.02-0.2 0.01h-0.19-0.44l-0.24-0.01-0.24-0.02-0.23-0.01-0.46-0.06-0.22-0.04-0.44-0.09-0.21-0.06-0.21-0.05-0.21-0.07-0.21-0.08-0.4-0.16-0.2-0.1-0.19-0.1-0.2-0.1-0.19-0.11-0.19-0.12-0.36-0.26v5.49l-0.02 0.2-0.01 0.09-0.02 0.09-0.06 0.24-0.03 0.07-0.04 0.07-0.03 0.05-0.04 0.06-0.08 0.1-0.1 0.08-0.06 0.02-0.05 0.03-0.06 0.02-0.06 0.01-0.07 0.01h-0.14-0.06l-0.07-0.02-0.05-0.02-0.06-0.02-0.11-0.06-0.04-0.04-0.05-0.04-0.08-0.1-0.04-0.06-0.03-0.06-0.02-0.07-0.03-0.08-0.04-0.16-0.02-0.1-0.01-0.09-0.01-0.11v-0.1-16.31-0.1l0.01-0.09 0.01-0.1 0.01-0.09 0.01-0.08 0.02-0.08 0.03-0.07 0.02-0.08 0.05-0.12 0.04-0.06 0.03-0.05 0.04-0.05 0.04-0.03 0.05-0.04 0.04-0.03 0.05-0.03 0.11-0.03 0.06-0.01h0.09l0.03 0.01h0.03zm4.49 1.74-0.46 0.04-0.22 0.03-0.21 0.03-0.21 0.05-0.38 0.1-0.36 0.14-0.15 0.07-0.16 0.08-0.15 0.1-0.17 0.11-0.16 0.12-0.17 0.14-0.17 0.15-0.18 0.16-0.18 0.18-0.2 0.19-0.2 0.2v6.54h0.06v-0.03l0.47 0.28 0.45 0.25 0.45 0.24 0.42 0.19 0.41 0.18 0.4 0.14 0.38 0.12 0.36 0.09 0.35 0.07 0.34 0.04 0.32 0.02 0.28-0.01 0.27-0.01 0.26-0.03 0.24-0.03 0.25-0.04 0.23-0.05 0.22-0.06 0.21-0.07 0.2-0.08 0.19-0.09 0.17-0.1 0.21-0.13 0.18-0.15 0.17-0.15 0.14-0.16 0.12-0.17 0.1-0.19 0.08-0.19 0.06-0.2 0.05-0.22 0.02-0.22 0.01-0.23v-4.75l-0.01-0.28-0.03-0.26-0.05-0.23-0.07-0.23-0.09-0.21-0.11-0.2-0.14-0.18-0.15-0.17-0.18-0.15-0.2-0.15-0.22-0.12-0.18-0.09-0.19-0.09-0.2-0.06-0.21-0.07-0.22-0.05-0.24-0.05-0.24-0.04-0.26-0.03-0.27-0.02-0.28-0.01-0.29-0.01-0.25 0.01h-0.26z'
id='d'
/>
</defs>
<use fill='#ffffff' xlinkHref='#k' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#k' />
<use fill='#ffffff' xlinkHref='#j' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#j' />
<use fill='#ffffff' xlinkHref='#b' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#b' />
<use fill='#ffffff' xlinkHref='#n' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#n' />
<use fill='#ffffff' xlinkHref='#l' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#l' />
<use fill='#ffffff' xlinkHref='#c' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#c' />
<use fill='#ffffff' xlinkHref='#e' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#e' />
<use fill='#ffffff' xlinkHref='#m' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#m' />
<use fill='#ffffff' xlinkHref='#a' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#a' />
<use fill='#ffffff' xlinkHref='#i' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#i' />
<use fill='#ffffff' xlinkHref='#f' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#f' />
<use fill='#ffffff' xlinkHref='#g' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#g' />
<use fill='#ffffff' xlinkHref='#h' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#h' />
<use fill='#ffffff' xlinkHref='#o' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#o' />
<use fill='#ffffff' xlinkHref='#d' />
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#d' />
</svg>
);
}
freeCodeCampLogo.displayName = 'freeCodeCampLogo';
export default freeCodeCampLogo;

View File

@ -13,7 +13,7 @@ const List = styled.div`
const filenameTitle = { fontWeight: '600' };
const FilenameResults = ({ searchValue, results }) => {
const FilenameResults = ({ searchValue, results, rateLimitMessage }) => {
const elements = results.map(result => {
const { filename, prs: prObjects } = result;
const prs = prObjects.map(({ number, username, title }, index) => {
@ -31,11 +31,17 @@ const FilenameResults = ({ searchValue, results }) => {
</Result>
);
});
const showResults = () => {
if (!rateLimitMessage) {
return (results.length ? <h3>Results for: {searchValue}</h3> : null) && elements;
} else {
return rateLimitMessage;
}
};
return (
<FullWidthDiv>
{results.length ? <h3>Results for: {searchValue}</h3> : null}
{elements}
{showResults()}
</FullWidthDiv>
);
};

View File

@ -16,8 +16,10 @@ const prNumStyle = { flex: 1 };
const usernameStyle = { flex: 1 };
const titleStyle = { flex: 3 };
const ListItem = ({ number, username, prTitle: title }) => {
const prUrl = `https://github.com/freeCodeCamp/freeCodeCamp/pull/${number}`;
const ListItem = ({ number, username, prTitle: title, prLink }) => {
const prUrl = prLink
? prLink
: `https://github.com/freeCodeCamp/freeCodeCamp/pull/${number}`;
return (
<Container>
<a

View File

@ -26,13 +26,14 @@ class Pareto extends React.Component {
all: [],
selectedFileType: 'all',
selectedLanguage: 'all',
options: {}
options: {},
rateLimitMessage: ''
};
componentDidMount() {
fetch(ENDPOINT_PARETO)
.then(response => response.json())
.then(({ ok, pareto }) => {
.then(({ ok, rateLimitMessage, pareto }) => {
if (ok) {
if (!pareto.length) {
pareto.push({
@ -41,12 +42,16 @@ class Pareto extends React.Component {
prs: []
});
}
this.setState(prevState => ({
data: pareto,
all: [...pareto],
options: this.createOptions(pareto)
}));
} else if (rateLimitMessage) {
this.setState(prevState => ({
rateLimitMessage
}));
}
})
.catch(() => {
@ -74,8 +79,8 @@ class Pareto extends React.Component {
handleFileTypeOptionChange = changeEvent => {
let { all, selectedLanguage, options } = this.state;
const selectedFileType = changeEvent.target.value;
let data = [ ...all ].filter(({ filename }) => {
let data = [...all].filter(({ filename }) => {
const { articleType, language } = this.getFilenameOptions(filename);
let condition;
if (selectedFileType === 'all') {
@ -86,7 +91,7 @@ class Pareto extends React.Component {
condition = articleType === selectedFileType;
} else if (!options[selectedFileType][selectedLanguage]) {
condition = articleType === selectedFileType
selectedLanguage = 'all';
selectedLanguage = 'all';
} else {
condition = articleType === selectedFileType && language === selectedLanguage
}
@ -95,11 +100,11 @@ class Pareto extends React.Component {
});
this.setState(prevState => ({ data, selectedFileType, selectedLanguage }));
}
handleLanguageOptionChange = changeEvent => {
const { all, selectedFileType } = this.state;
const selectedLanguage = changeEvent.target.value;
let data = [ ...all ].filter(({ filename }) => {
let data = [...all].filter(({ filename }) => {
const { articleType, language } = this.getFilenameOptions(filename);
let condition;
if (selectedLanguage === 'all') {
@ -122,33 +127,34 @@ class Pareto extends React.Component {
const [_, articleType, language] = filenameReplacement.match(regex) || [];
return { articleType, language };
}
render() {
const { data, options, selectedFileType, selectedLanguage } = this.state;
const elements = data.map(entry => {
const { filename, count, prs } = entry;
const prsList = prs.map(({ number, username, title }) => {
return <ListItem key={number} number={number} username={username} prTitle={title} />;
});
const fileOnMaster = `https://github.com/freeCodeCamp/freeCodeCamp/blob/master/${filename}`;
return (
<Result key={filename}>
<span style={filenameTitle}>{filename}</span>{' '}
<a href={fileOnMaster} rel="noopener noreferrer" target="_blank">
(File on Master)
render() {
const { data, options, selectedFileType, selectedLanguage, rateLimitMessage } = this.state;
const elements = rateLimitMessage
? rateLimitMessage
: data.map(entry => {
const { filename, count, prs } = entry;
const prsList = prs.map(({ number, username, title }) => {
return <ListItem key={number} number={number} username={username} prTitle={title} />;
});
const fileOnMaster = `https://github.com/freeCodeCamp/freeCodeCamp/blob/master/${filename}`;
return (
<Result key={filename}>
<span style={filenameTitle}>{filename}</span>{' '}
<a href={fileOnMaster} rel="noopener noreferrer" target="_blank">
(File on Master)
</a>
<br />
<details style={detailsStyle}>
<summary># of PRs: {count}</summary>
<List>{prsList}</List>
</details>
</Result>
);
});
<br />
<details style={detailsStyle}>
<summary># of PRs: {count}</summary>
<List>{prsList}</List>
</details>
</Result>
);
});
let fileTypeOptions = Object.keys(options).map(articleType => articleType);
const typeOptions = [ 'all', ...fileTypeOptions].map((type) => (
const typeOptions = ['all', ...fileTypeOptions].map((type) => (
<FilterOption
key={type}
name="filetype"
@ -156,13 +162,13 @@ class Pareto extends React.Component {
onOptionChange={this.handleFileTypeOptionChange}
selectedOption={selectedFileType}
>
{type.charAt().toUpperCase() + type.slice(1)}
{type.charAt().toUpperCase() + type.slice(1)}
</FilterOption>
));
let languageOptions = null;
if (selectedFileType !== 'all') {
let languages = Object.keys(options[selectedFileType]);
let languages = Object.keys(options[selectedFileType]);
languages = ['all', ...languages.sort()];
languageOptions = languages.map(language => (
<FilterOption
@ -172,9 +178,9 @@ class Pareto extends React.Component {
onOptionChange={this.handleLanguageOptionChange}
selectedOption={selectedLanguage}
>
{language.charAt().toUpperCase() + language.slice(1)}
{language.charAt().toUpperCase() + language.slice(1)}
</FilterOption>
));
));
}
return (
<FullWidthDiv>
@ -198,7 +204,11 @@ class Pareto extends React.Component {
</fieldset>
}
</Options>
{data.length ? elements : 'Report Loading...'}
{rateLimitMessage
? rateLimitMessage
: data.length
? elements
: 'Report Loading...'}
</FullWidthDiv>
);
}

View File

@ -9,7 +9,7 @@ const List = styled.ul`
margin: 3px;
`;
const PrResults = ({ searchValue, results }) => {
const PrResults = ({ searchValue, results, rateLimitMessage }) => {
const elements = results.map((result, idx) => {
const { number, filenames, username, title } = result;
const files = filenames.map((filename, index) => {
@ -32,10 +32,17 @@ const PrResults = ({ searchValue, results }) => {
);
});
const showResults = () => {
if (!rateLimitMessage) {
return (results.length ? <h3>Results for PR# {searchValue}</h3> : null) && elements;
} else {
return rateLimitMessage;
}
};
return (
<FullWidthDiv style={{ width: '100%' }}>
{results.length ? <h3>Results for PR# {searchValue}</h3> : null}
{elements}
{showResults()}
</FullWidthDiv>
);
};

View File

@ -0,0 +1,92 @@
import React from 'react';
import styled from 'styled-components';
import ListItem from './ListItem';
import FullWidthDiv from './FullWidthDiv';
import Result from './Result';
import { ENDPOINT_ALL_REPOS } from '../constants';
const List = styled.div`
margin: 5px;
display: flex;
flex-direction: column;
`;
const detailsStyle = { padding: '3px' };
const filenameTitle = { fontWeight: '600' };
class Repos extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
rateLimitMessage: ''
}
}
componentDidMount() {
fetch(ENDPOINT_ALL_REPOS)
.then(response => response.json())
.then(({ ok, rateLimitMessage, allRepos }) => {
if (ok) {
const repos = allRepos.filter(this.props.dataFilter);
if (!repos.length) {
repos.push({
repoName: 'No repos with open PRs',
prs: []
});
}
this.setState(prevState => ({
data: repos
}));
} else if (rateLimitMessage) {
this.setState(prevState => ({
rateLimitMessage
}));
}
})
.catch(() => {
const repos = [
{ repoName: 'No repos with open PRs', prs: [] }
];
this.setState(prevState => ({ data: repos }));
});
}
render() {
const { data, rateLimitMessage } = this.state;
const elements = rateLimitMessage
? rateLimitMessage
: data.map(entry => {
const { _id: repoName, prs } = entry;
const prsList = prs.map(({ _id: number, username, title, prLink }) => {
return <ListItem key={number} number={number} username={username} prTitle={title} prLink={prLink} />;
});
return (
<Result key={repoName}>
<span style={filenameTitle}>{repoName}</span>
<br />
<details style={detailsStyle}>
<summary># of PRs: {prs.length}</summary>
<List>{prsList}</List>
</details>
</Result>
);
});
return (
<FullWidthDiv>
{rateLimitMessage
? rateLimitMessage
: data.length
? elements
: 'Report Loading...'}
</FullWidthDiv>
);
}
}
export default Repos;

View File

@ -11,7 +11,7 @@ class Search extends Component {
searchValue: '',
selectedOption: 'pr',
results: [],
message: ''
message: '',
};
clearObj = { searchValue: '', results: [] };
@ -64,9 +64,13 @@ class Search extends Component {
fetch(fetchUrl)
.then(response => response.json())
.then(({ ok, message, results }) => {
.then(({ ok, message, results, rateLimitMessage }) => {
if (ok) {
this.setState(prevState => ({ message, results }));
} else if (rateLimitMessage) {
this.setState(prevState => ({
rateLimitMessage
}));
}
})
.catch(() => {
@ -86,7 +90,7 @@ class Search extends Component {
handleOptionChange,
state
} = this;
const { searchValue, message, results, selectedOption } = state;
const { searchValue, message, results, selectedOption, rateLimitMessage } = state;
return (
<>
@ -114,10 +118,10 @@ class Search extends Component {
<button onClick={handleButtonClick}>Search</button>
{message}
{selectedOption === 'pr' && (
<PrResults searchValue={searchValue} results={results} />
<PrResults searchValue={searchValue} results={results} rateLimitMessage={rateLimitMessage} />
)}
{selectedOption === 'filename' && (
<FilenameResults searchValue={searchValue} results={results} />
<FilenameResults searchValue={searchValue} results={results} rateLimitMessage={rateLimitMessage} />
)}
</>
);

View File

@ -20,8 +20,8 @@ const Tab = styled.div`
&:hover {
cursor: pointer;
background: #eeeeee;
color: ${({ theme }) => theme.primary};
background: ${({ theme }) => theme.primary};
color: white;
}
&:first-child {
@ -42,6 +42,12 @@ const Tabs = ({ view, onViewChange }) => {
<Tab id="tabs-reports" onClick={onViewChange} active={view === 'reports'}>
Pareto
</Tab>
<Tab id="tabs-boilerplates" onClick={onViewChange} active={view === 'boilerplates'}>
Boilerplate PRs
</Tab>
<Tab id="tabs-other" onClick={onViewChange} active={view === 'other'}>
Other Repos' PRs
</Tab>
</Container>
);
};

View File

@ -1,12 +1,14 @@
require('dotenv').config();
let API_HOST = process.env.REACT_APP_HOST === 'local'
? 'http://localhost:3001'
: '';
API_HOST += '/dashboard';
const ENDPOINT_INFO = API_HOST + '/info';
const ENDPOINT_PARETO = API_HOST + '/pareto';
const ENDPOINT_ALL_REPOS = API_HOST + '/all-repos';
const ENDPOINT_PR = API_HOST + '/pr';
const ENDPOINT_SEARCH = API_HOST + '/search';
export {
API_HOST, ENDPOINT_INFO, ENDPOINT_PARETO, ENDPOINT_PR, ENDPOINT_SEARCH
API_HOST, ENDPOINT_INFO, ENDPOINT_PARETO, ENDPOINT_PR, ENDPOINT_SEARCH, ENDPOINT_ALL_REPOS
};

View File

@ -47,13 +47,13 @@ body {
}
.app-menu li a:hover {
color: #006400;
background: #eee;
color: #0a0a23;
background: white;
}
a {
text-decoration: none;
color: #006400;
color: #0a0a23;
}
a:visited {

View File

@ -1,5 +1,5 @@
const theme = {
primary: '#006400'
primary: '#0a0a23'
};
export default theme;

View File

@ -0,0 +1,69 @@
const express = require('express');
const mongoose = require('mongoose');
const path = require('path');
const app = express();
const config = require('../../lib/config');
const { pareto, pr, search, info, allRepos } = require('./routes');
const { reqLimiter } = require('./req-limiter');
// May need to uncomment the following to get rateLimit to work properly since we are using reverse-proxy
// app.set('trust proxy', 1);
app.use(express.static(path.join(__dirname, '../client/build')));
app.use((request, response, next) => {
response.header('Access-Control-Allow-Origin', '*');
response.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
response.header('Access-Control-Allow-Methods', 'GET');
next();
});
const landingPage = path.join(__dirname, '../client/build/index.html');
app.get('/', reqLimiter, (req, res) => res.sendFile(landingPage));
app.use('/pr', pr);
app.use('/search', search);
app.use('/pareto', pareto);
app.use('/info', info);
app.use('/all-repos', allRepos);
// 404
app.use(function(req, res) {
const message = 'Route' + req.url + ' Not found.';
console.log(message);
return res.status(404).send({ message });
});
// 500 - Any server error
app.use(function(err, req, res) {
console.log('error: ' + err);
return res.status(500).send({ error: err });
});
if (mongoose.connection.readyState === 0) {
// connect to mongo db
const mongoUri = config.mongo.host;
const promise = mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
promise
.then(() => {
console.log('MongoDB is connected');
const portNum = process.env.PORT || 3000;
app.listen(portNum, () => {
console.log(`server listening on port ${portNum}`);
});
})
.catch(err => {
console.log(err);
console.log('MongoDB connection unsuccessful');
});
}
module.exports = app;

View File

@ -14,11 +14,30 @@ const info = new mongoose.Schema({
prRange: String
});
const allRepos = new mongoose.Schema({
_id: String,
prs: [
{
_id: Number,
title: String,
username: String,
prLink: String
}
]
});
const dbCollections = {
pr: 'openprs',
info: 'info'
pr: 'openprs',
info: 'info',
boilerplate: 'boilerplate',
otherPrs: 'otherPrs'
};
const PR = mongoose.model('PR', pr, dbCollections['pr']);
const INFO = mongoose.model('INFO', info, dbCollections['info']);
module.exports = { PR, INFO, dbCollections };
const ALL_REPOS = mongoose.model(
'ALL_REPOS',
allRepos,
dbCollections['allRepos']
);
module.exports = { PR, INFO, ALL_REPOS, dbCollections };

View File

@ -1,46 +1,39 @@
{
"name": "@freecodecamp/contribute",
"name": "@freecodecamp/contributer-tools",
"version": "1.0.0",
"license": "BSD-3-Clause",
"repository": "https://github.com/freecodecamp/contribute.git",
"bugs": "https://github.com/freeCodeCamp/contribute/issues",
"repository": "https://github.com/freecodecamp/dashboard.git",
"bugs": "https://github.com/freeCodeCamp/freeCodeCamp/issues",
"keywords": [
"probot",
"freeCodeCamp",
"github",
"probot-app"
"contributer tools"
],
"scripts": {
"dev": "develop",
"develop": "nodemon",
"start": "probot run esm ./index.js",
"start-probot": "cross-env TEST_ENV=false probot run ./index.js",
"start": "node index.js",
"lint": "standard --fix",
"test": "cross-env TEST_ENV=true jest --forceExit && standard",
"test:watch": "jest --watch --notify --notifyMode=change --coverage"
},
"dependencies": {
"bluebird": "^3.5.3",
"@octokit/rest": "^18.0.12",
"body-parser": "^1.18.3",
"cli-progress": "^2.1.1",
"cross-env": "^5.2.0",
"dotenv": "^6.2.0",
"express": "^4.16.3",
"http-status": "^1.3.1",
"joi": "^14.3.1",
"mongoose": "^5.4.1",
"node-fetch": "^2.3.0",
"probot": "^7.2.0"
"express-rate-limit": "^5.2.3",
"mongoose": "^5.4.1"
},
"devDependencies": {
"expect": "^23.6.0",
"jest": "^22.4.3",
"nock": "^10.0.0",
"nodemon": "^1.17.2",
"smee-client": "^1.0.2",
"standard": "^10.0.3"
},
"engines": {
"node": ">= 8.3.0"
"node": ">= 12.18.3"
},
"standard": {
"env": [

View File

@ -0,0 +1,23 @@
const rateLimit = require("express-rate-limit");
const limitHandler = (req, res) => {
res
.status(429)
.json({
ok: false,
rateLimitMessage: 'You have accessed this app\'s pages too quickly. Please try again in 5 minutes.'
}
);
};
const rateLimitOptions = {
windowMs: 5 * 60 * 1000, // 5 minutes
max: 100,
message: 'rate limit activated',
handler: limitHandler,
onLimitReached: limitHandler
};
const reqLimiter = rateLimit(rateLimitOptions);
module.exports = { reqLimiter };

View File

@ -0,0 +1,20 @@
const router = require('express').Router();
const { ALL_REPOS } = require('../models');
const { reqLimiter } = require('../req-limiter');
router.get('/', reqLimiter, async (request, response) => {
let allRepos = await ALL_REPOS.find({}).then(data => data);
allRepos.sort((a, b) => a._id - b._id);
allRepos = allRepos.reduce((allReposArr, aRepo) => {
const { _id, prs } = aRepo;
if (prs.length) {
prs.sort((a, b) => a._id - b._id);
return allReposArr.concat({ _id, prs });
}
return allRepos;
}, []);
response.json({ ok: true, allRepos });
});
module.exports = router;

View File

@ -2,5 +2,6 @@ const pareto = require('./pareto');
const pr = require('./pr');
const search = require('./search');
const info = require('./info');
const allRepos = require('./all-repos');
module.exports = { pareto, pr, search, info };
module.exports = { pareto, pr, search, info, allRepos };

View File

@ -1,7 +1,8 @@
const router = require('express').Router();
const { INFO } = require('../models');
const { reqLimiter } = require('../req-limiter');
router.get('/', async (request, response) => {
router.get('/', reqLimiter, async (request, response) => {
const [{ lastUpdate, numPRs, prRange }] = await INFO.find({});
response.json({ ok: true, lastUpdate, numPRs, prRange });
});

View File

@ -1,5 +1,6 @@
const router = require('express').Router();
const { PR } = require('../models');
const { reqLimiter } = require('../req-limiter');
const createPareto = reportObj =>
Object.keys(reportObj)
@ -12,7 +13,7 @@ const createPareto = reportObj =>
}, [])
.sort((a, b) => b.count - a.count);
router.get('/', async (reqeust, response) => {
router.get('/', reqLimiter, async (reqeust, response) => {
const prs = await PR.find({}).then(data => data);
prs.sort((a, b) => a._id - b._id);
const reportObj = prs.reduce((obj, pr) => {

View File

@ -1,7 +1,8 @@
const router = require('express').Router();
const { PR } = require('../models');
const { reqLimiter } = require('../req-limiter');
router.get('/:number', async (request, response) => {
router.get('/:number', reqLimiter, async (request, response) => {
const prs = await PR.find({}).then(data => data);
prs.sort((a, b) => a._id - b._id);
const indices = prs.reduce((obj, { _id }, index) => {

View File

@ -1,7 +1,8 @@
const router = require('express').Router();
const { PR } = require('../models');
const { reqLimiter } = require('../req-limiter');
router.get('/', async (request, response) => {
router.get('/', reqLimiter, async (request, response) => {
const prs = await PR.find({}).then(data => data);
prs.sort((a, b) => a._id - b._id);
const indices = prs.reduce((obj, { _id }, index) => {

View File

@ -0,0 +1,27 @@
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret, freeCodeCampRepo, defaultBase }
} = require('../../../lib/config');
const getPRs = async () => {
const octokit = new Octokit({ auth: secret });
/* eslint-disable camelcase */
const methodProps = {
owner,
repo: freeCodeCampRepo,
base: defaultBase,
state: 'open',
sort: 'created',
direction: 'asc',
per_page: 100
};
const openPRs = await octokit.paginate(octokit.pulls.list, methodProps);
// const openPRs = await octokit.pulls.list(methodProps);
// console.log(openPRs[0])
return openPRs;
};
module.exports = getPRs;

View File

@ -0,0 +1,54 @@
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret }
} = require('../../../lib/config');
const getRepos = async () => {
const octokit = new Octokit({ auth: secret });
/* eslint-disable camelcase */
const methodProps = {
org: owner,
sort: 'full_name',
direction: 'asc',
per_page: 100
};
const repos = await octokit.paginate(octokit.repos.listForOrg, methodProps);
const otherRepos = repos
.filter(repo => !repo.archived && repo.name !== owner)
.map(repo => repo.name);
const reposToAdd = [];
for (let repo of otherRepos) {
const methodProps = {
owner,
repo,
state: 'open',
sort: 'created',
direction: 'asc',
page: 1,
per_page: 100
};
const openPRs = await octokit.paginate(octokit.pulls.list, methodProps);
if (openPRs.length) {
const prsToAdd = [];
for (let pr of openPRs) {
const {
number,
title,
user: { login: username },
html_url: prLink
} = pr;
prsToAdd.push({ _id: number, title, username, prLink });
}
reposToAdd.push({ _id: repo, prs: prsToAdd });
}
}
return reposToAdd;
};
module.exports = getRepos;

View File

@ -0,0 +1,27 @@
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret, freeCodeCampRepo }
} = require('../../../lib/config');
const octokit = new Octokit({ auth: secret });
const getFiles = async number => {
/* eslint-disable camelcase */
const methodProps = {
owner,
repo: freeCodeCampRepo,
pull_number: number,
per_page: 100
};
const files = await octokit.paginate(octokit.pulls.listFiles, methodProps);
return files;
};
const getFilenames = async number => {
const files = await getFiles(number);
return files.map(({ filename }) => filename);
};
module.exports = getFilenames;

View File

@ -1,34 +1,37 @@
const config = require('../../../config');
// config should be imported before importing any other file
const mongoose = require('mongoose');
const getRepos = require('./get-repos');
const getPRs = require('./get-prs');
const getFilenames = require('./getFilenames');
const { PR, INFO, ALL_REPOS } = require('../models');
const { mongo } = require('../../../lib/config');
// added to prevent deprecation warning when findOneAndUpdate is used
mongoose.set('useFindAndModify', false);
// connect to mongo db
const mongoUri = config.mongo.host;
const db = mongoose.connect(
mongoUri,
{ useNewUrlParser: true }
);
const { PR, INFO } = require('../models');
const { getPRs, getUserInput, getFilenames } = require('../../../lib/get-prs');
const { rateLimiter } = require('../../../lib/utils');
const mongoUri = mongo.host;
const db = mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
const lastUpdate = new Date();
db.then(async () => {
const reposToAdd = await getRepos();
await ALL_REPOS.deleteMany();
await ALL_REPOS.insertMany(reposToAdd);
// update PRs for freeCodeCamp repo
const oldPRs = await PR.find({}).then(data => data);
const oldIndices = oldPRs.reduce((obj, { _id }, index) => {
obj[_id] = index;
return obj;
}, {});
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
const prPropsToGet = ['number', 'user', 'title', 'updated_at'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
let count = 0;
const openPRs = await getPRs();
const newIndices = {};
for (let i = 0; i < openPRs.length; i++) {
const {
@ -44,25 +47,20 @@ db.then(async () => {
if (!oldIndices.hasOwnProperty(number)) {
// insert a new pr
const filenames = await getFilenames(number);
count++;
await PR.create({ _id: number, updatedAt, title, username, filenames });
console.log('added PR# ' + number);
} else if (updatedAt > oldUpdatedAt) {
// update an existing pr
const filenames = await getFilenames(number);
count++;
await PR.findOneAndUpdate(
{ _id: number },
{ updatedAt, title, username, filenames }
);
console.log('updated PR #' + number);
}
if (count > 4500) {
await rateLimiter(4500);
}
}
for (let j = 0; j < oldPRs.length; j++) {
const { _id: number } = oldPRs[j];
for (let pr of oldPRs) {
const { _id: number } = pr;
if (!newIndices.hasOwnProperty(number)) {
// delete pr because it is no longer open
await PR.deleteOne({ _id: number });
@ -72,25 +70,24 @@ db.then(async () => {
})
.then(async () => {
// update info collection
const [ { firstPR, lastPR }] = await PR.aggregate(
[{
const [{ firstPR, lastPR }] = await PR.aggregate([
{
$group: {
_id: null,
firstPR: { $min: '$_id' },
lastPR: { $max: '$_id' }
}
}]
);
}
]);
const numPRs = await PR.countDocuments();
const info = {
lastUpdate,
numPRs,
prRange: `${firstPR}-${lastPR}`
};
await INFO.updateOne({}, info, { upsert: true })
.catch(err => {
console.log(err);
});
await INFO.updateOne({}, info, { upsert: true }).catch(err => {
console.log(err);
});
mongoose.connection.close();
})
.catch(err => {

View File

@ -0,0 +1,64 @@
## Local Setup
- Follow the steps below to get this running on your local machine
### 1. Copy .env
- Copy the `sample.env` file into `.env`. The command below will do that in the terminal if your CWD(current working directory) is the `contribute` folder.
```bash
cp sample.env .env
```
- If you do not want to populate the database with the freeCodeCamp PR's you can [skip to step 5](#5-start-the-app-in-developemnt-mode)
### 2. Update .env
- Use your GitHub username as the `GITHUB_USERNAME` variable of the `.env` file
- Use your GitHub Personal Access Token as the `GITHUB_ACCESS_TOKEN` variable of the `.env` file
### 3. Run mongoDB
- Make sure a mongoDB instance is running by running the command below in the terminal.
```bash
mongod —dbpath=./database_folder
```
### 4. Update the Database
- Run the command below to populate your local database with PRs from the freeCodeCamp repo. Note that you must have mongoDB running.
```bash
node dashboard-app/server/tools/update-db.js
```
- This will take a while. If it stops running partway through, it's probably a timeout error. Run the command again and it should finish
### 5. Start the app in development mode
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
```bash
npm ci
npm run develop
```
### 6. Start the app in production mode
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
```bash
npm ci
npm run build
npm start
```
## Caveats & Notes
### Local Ports when developing locally
Using `npm run develop` will start both the api server and the Create React App(Dashboard) in development mode. The app server runs on port 3001 and the React app runs on port 3000.
### The one-off scripts will error out on actions performed by repository admins
For example, if an admin removes a label from a Pull Request, the script can not add that label back. This is usually because the script is acting on behalf of a non-admin user with write access.
This is usually the case with the use of access tokens for scripts.
### Setting up Cron jobs for Sweeper Scripts
For updating dashboard data we use PM2 like so:
```bash
pm2 start --no-autorestart dashboard-app/server/tools/update-db.js --cron "*/10 * * * *"
```
This will start the script in the "no restart" mode and re-run it every 10 minutes.
A useful link to calculate a Cron expression: <https://crontab.guru/every-10-minutes>
### Starting the express server (via probot)
```bash
pm2 start "npm start" --name "contribute-app"
```
**Note:** Start only one instance of this app, you can't have multiple probot apps running. Starting multiple instances will crash the app.

View File

@ -0,0 +1,4 @@
{
"packages": ["dashboard-app", "dashboard-app/client", "dashboard-app/server", "lib", "one-off-scripts"],
"version": "independent"
}

View File

@ -1,8 +1,6 @@
const Joi = require('joi');
const path = require('path');
// require and configure dotenv, will load vars in .env in PROCESS.ENV
const envPath = path.resolve(__dirname, '../probot/.env');
require('dotenv').config({ path: envPath });
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
// define validation for all the env vars
const envVarsSchema = Joi.object({
@ -13,16 +11,12 @@ const envVarsSchema = Joi.object({
MONGO_HOST: Joi.string()
.required()
.description('Mongo DB host url'),
MONGO_PORT: Joi.number().default(27017),
GITHUB_USERNAME: Joi.string().required(),
GITHUB_ACCESS_TOKEN: Joi.string().required(),
REPOSITORY_OWNER: Joi.string().required(),
REPOSITORY: Joi.string().required(),
PRODUCTION_RUN: Joi.boolean()
.default(false),
WEBHOOK_PROXY_URL: Joi.string().required(),
APP_ID: Joi.number().required(),
WEBHOOK_SECRET: Joi.string().required()
DEFAULT_BASE: Joi.string().required(),
PRODUCTION_RUN: Joi.boolean().default(false)
})
.unknown()
.required();
@ -35,19 +29,14 @@ if (error) {
const config = {
env: envVars.NODE_ENV,
mongo: {
host: envVars.MONGO_HOST,
port: envVars.MONGO_PORT
host: envVars.MONGO_HOST
},
github: {
id: envVars.GITHUB_USERNAME,
secret: envVars.GITHUB_ACCESS_TOKEN,
owner: envVars.REPOSITORY_OWNER,
repo: envVars.REPOSITORY,
probot: {
webhookUrl: envVars.WEBHOOK_PROXY_URL,
webhookSecret: envVars.WEBHOOK_SECRET,
appID: envVars.APP_ID
}
freeCodeCampRepo: envVars.REPOSITORY,
defaultBase: envVars.DEFAULT_BASE
},
oneoff: {
productionRun: envVars.PRODUCTION_RUN

View File

@ -1,30 +1,37 @@
const { Octokit } = require('@octokit/rest');
const _cliProgress = require('cli-progress');
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
const {
github: { owner, secret }
} = require('../config');
const octokit = new Octokit({ auth: secret });
const octokit = require('@octokit/rest')(octokitConfig);
const { getRange, getCount } = require('./pr-stats');
octokit.authenticate(octokitAuth);
const methodProps = {
owner,
/* eslint-disable camelcase */
const prsPaginate = async (
repo,
state: 'open',
base: 'master',
sort: 'created',
direction: 'asc',
page: 1,
per_page: 100
};
const prsPaginate = async(
base,
method,
firstPR,
lastPR,
prPropsToGet,
progressBar
prPropsToGet
) => {
let methodProps = {
owner,
repo,
state: 'open',
sort: 'created',
direction: 'asc',
page: 1,
per_page: 100
};
if (base) {
methodProps = { ...methodProps, base };
}
const prFilter = (prs, first, last, prPropsToGet) => {
const filtered = [];
for (let pr of prs) {
@ -46,26 +53,31 @@ const prsPaginate = async(
// will be true when lastPR is seen in paginated results
let done = false;
let response = await method(methodProps);
console.log('x-ratelimit-remaining:', response.meta['x-ratelimit-remaining']);
let { data } = response;
data = prFilter(data, firstPR, lastPR, prPropsToGet);
while (octokit.hasNextPage(response) && !done) {
response = await octokit.getNextPage(response);
console.log(
'x-ratelimit-remaining:',
response.meta['x-ratelimit-remaining']
);
let dataFiltered = prFilter(response.data, firstPR, lastPR, prPropsToGet);
data = data.concat(dataFiltered);
progressBar.increment(dataFiltered.length);
// progressBar.increment(dataFiltered.length);
}
return data;
};
const getUserInput = async(rangeType = '') => {
const getUserInput = async (repo, base, rangeType = '') => {
let data, firstPR, lastPR;
if (rangeType === 'all') {
data = await getRange().then(data => data);
data = await getRange(repo, base).then(data => data);
firstPR = data[0];
lastPR = data[1];
} else {
let [type, start, end] = process.argv.slice(2);
data = await getRange().then(data => data);
data = await getRange(repo, base).then(data => data);
firstPR = data[0];
lastPR = data[1];
if (type !== 'all' && type !== 'range') {
@ -90,11 +102,15 @@ const getUserInput = async(rangeType = '') => {
lastPR = end;
}
}
const totalPRs = await getCount().then(data => data);
// A null value for firstPR or lastPR indicates the repo had no open PRs
if (firstPR === null || lastPR === null) {
return { totalPRs: 0, firstPR, lastPR };
}
const totalPRs = await getCount(repo, base).then(data => data);
return { totalPRs, firstPR, lastPR };
};
const getPRs = async (totalPRs, firstPR, lastPR, prPropsToGet) => {
const getPRs = async (repo, base, totalPRs, firstPR, lastPR, prPropsToGet) => {
let progressText = `Retrieve PRs (${firstPR}-${lastPR}) [{bar}] `;
progressText += '{percentage}% | Elapsed Time: {duration_formatted} ';
progressText += '| ETA: {eta_formatted}';
@ -105,24 +121,39 @@ const getPRs = async (totalPRs, firstPR, lastPR, prPropsToGet) => {
},
_cliProgress.Presets.shades_classic
);
getPRsBar.start(totalPRs, 0);
// getPRsBar.start(totalPRs, 0);
let openPRs = await prsPaginate(
octokit.pullRequests.list,
repo,
base,
octokit.pulls.list,
firstPR,
lastPR,
prPropsToGet,
getPRsBar
);
getPRsBar.update(totalPRs);
getPRsBar.stop();
// getPRsBar.update(totalPRs);
// getPRsBar.stop();
console.log(`# of PRs retrieved: ${openPRs.length}`);
return { firstPR, lastPR, openPRs };
};
const filesPaginate = async (repo, number) => {
let methodProps = {
owner,
state: 'open',
sort: 'created',
direction: 'asc',
page: 1,
per_page: 100
};
const filesPaginate = async number => {
let response = await octokit.pullRequests.listFiles({
number, ...methodProps
if (repo) {
methodProps = { ...methodProps, repo };
}
let response = await octokit.pulls.listFiles({
number,
...methodProps
});
let { data } = response;
@ -134,10 +165,9 @@ const filesPaginate = async number => {
return data;
};
const getFiles = async number => await filesPaginate(number);
const getFiles = async (repo, number) => await filesPaginate(repo, number);
const getFilenames = async number => (
await getFiles(number)
).map(({ filename }) => filename);
const getFilenames = async (repo, number) =>
(await getFiles(repo, number)).map(({ filename }) => filename);
module.exports = { getPRs, getUserInput, getFiles, getFilenames };

View File

@ -0,0 +1,54 @@
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret }
} = require('../config');
const octokit = new Octokit({ auth: secret });
const getCount = async (repo, base) => {
const baseStr = base ? `+base:${base}` : '';
/* eslint-disable camelcase */
const {
data: { total_count: count }
} = await octokit.search
.issues({
q: `repo:${owner}/${repo}+is:open+type:pr${baseStr}`,
sort: 'created',
order: 'asc',
page: 1,
per_page: 1
})
.catch(err => console.log(err));
return count;
};
const getRange = async (repo, base) => {
let methodProps = {
owner,
repo,
state: 'open',
sort: 'created',
page: 1,
per_page: 1
};
if (base) {
methodProps = { ...methodProps, base };
}
let response = await octokit.pulls.list({
direction: 'asc',
...methodProps
});
// In the case there are no open PRs for repo
if (!response.data.length) {
return [null, null];
}
const firstPR = response.data[0].number;
response = await octokit.pulls.list({
direction: 'desc',
...methodProps
});
const lastPR = response.data[0].number;
return [firstPR, lastPR];
};
module.exports = { getCount, getRange };

1825
tools/contributor/lib/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "freecodecampcontributelib",
"dependencies": {
"@octokit/rest": "^15.18.0",
"@octokit/rest": "^18.0.12",
"cli-progress": "^2.1.0",
"date-fns": "^1.29.0",
"dedent": "^0.7.0",

View File

@ -1,14 +1,16 @@
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
const { Octokit } = require('@octokit/rest');
const octokit = require('@octokit/rest')(octokitConfig);
const {
github: { owner, secret, freeCodeCampRepo }
} = require('../config');
octokit.authenticate(octokitAuth);
const octokit = new Octokit({ auth: secret });
const addComment = async (number, comment) => {
const result = await octokit.issues
.createComment({
owner,
repo,
repo: freeCodeCampRepo,
number,
body: comment
})

View File

@ -1,11 +1,13 @@
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
const octokit = require('@octokit/rest')(octokitConfig);
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret, freeCodeCampRepo }
} = require('../config');
octokit.authenticate(octokitAuth);
const octokit = new Octokit({ auth: secret });
const addLabels = (number, labels, log) => {
octokit.issues
.addLabels({ owner, repo, number, labels })
.addLabels({ owner, repo: freeCodeCampRepo, number, labels })
.then(() => {
console.log(`PR #${number} added ${JSON.stringify(labels)}\n`);
})

View File

@ -1,29 +1,31 @@
const { addComment } = require('./add-comment');
const { rateLimiter } = require('../utils');
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
const {
github: { owner, secret, freeCodeCampRepo, defaultBase }
} = require('../config');
const octokit = require('@octokit/rest')(octokitConfig);
const { Octokit } = require('@octokit/rest');
octokit.authenticate(octokitAuth);
const octokit = new Octokit({ auth: secret });
/* closes and reopens an open PR with applicable comment */
const closeOpen = async number => {
await octokit.pullRequests
await octokit.pulls
.update({
owner,
repo,
repo: freeCodeCampRepo,
number,
state: 'closed',
base: 'master'
base: defaultBase
})
.then(async () => {
await rateLimiter(5000);
return octokit.pullRequests.update({
return octokit.pulls.update({
owner,
repo,
repo: freeCodeCampRepo,
number,
state: 'open',
base: 'master'
base: defaultBase
});
})
.then(async () => {

View File

@ -1,13 +1,9 @@
const config = require('../../config');
const config = require('../../lib/config');
const { validLabels } = require('../validation');
const { addLabels } = require('./add-labels');
const { rateLimiter } = require('../utils');
const labeler = async(
number,
prFiles,
currentLabels
) => {
const labeler = async (number, prFiles, currentLabels) => {
// holds potential labels to add based on file path
const labelsToAdd = {};
const existingLabels = currentLabels.map(({ name }) => name);
@ -20,7 +16,9 @@ const labeler = async(
);
const regex = /^(docs|curriculum)(?:\/)(english|arabic|chinese|portuguese|russian|spanish)?\/?/;
// need an array to pass to labelsAdder
const [_, articleType, language] = filenameReplacement.match(regex) || [];
const match = filenameReplacement.match(regex) || [];
const articleType = match[1];
const language = match[2];
if (articleType && validLabels[articleType]) {
labelsToAdd[validLabels[articleType]] = 1;
}

View File

@ -1,4 +1,4 @@
const config = require('../../config');
const config = require('../../lib/config');
const formatDate = require('date-fns/format');
const path = require('path');
const fs = require('fs');

View File

@ -4,10 +4,7 @@ a comment and "status: needs update" label to any PR with guide articles which
have frontmatter issues.
*/
const config = require('../config');
const fetch = require('node-fetch');
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
const { addLabels, addComment } = require('../lib/pr-tasks');
const { rateLimiter, ProcessingLog } = require('../lib/utils');
@ -18,6 +15,11 @@ const {
createErrorMsg
} = require('../lib/validation/guide-folder-checks/create-error-msg');
const {
github: { freeCodeCampRepo, defaultBase },
oneoff: { productionRun }
} = require('../config');
const allowedLangDirNames = [
'arabic',
'chinese',
@ -29,7 +31,7 @@ const allowedLangDirNames = [
const log = new ProcessingLog('all-frontmatter-checks');
const labeler = async(
const labeler = async (
number,
prFiles,
currentLabels,
@ -47,7 +49,7 @@ const labeler = async(
return !existingLabels.includes(label);
});
if (newLabels.length) {
if (config.oneoff.productionRun) {
if (productionRun) {
addLabels(number, newLabels);
await rateLimiter();
}
@ -68,11 +70,11 @@ const checkPath = (fullPath, fileContent) => {
return errorMsgs.concat(frontMatterErrMsgs);
};
const guideFolderChecks = async(number, prFiles, user) => {
const guideFolderChecks = async (number, prFiles, user) => {
let prErrors = [];
for (let { filename: fullPath, raw_url: fileUrl } of prFiles) {
let newErrors;
if ((/^guide\//).test(fullPath)) {
if (/^guide\//.test(fullPath)) {
const response = await fetch(fileUrl);
const fileContent = await response.text();
newErrors = checkPath(fullPath, fileContent);
@ -84,7 +86,7 @@ const guideFolderChecks = async(number, prFiles, user) => {
if (prErrors.length) {
const comment = createErrorMsg(prErrors, user);
if (config.oneoff.productionRun) {
if (productionRun) {
await addComment(number, comment);
await rateLimiter();
}
@ -94,10 +96,20 @@ const guideFolderChecks = async(number, prFiles, user) => {
}
};
(async() => {
const { totalPRs, firstPR, lastPR } = await getUserInput();
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase
);
const prPropsToGet = ['number', 'labels', 'user'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
log.start();
console.log('Starting frontmatter checks process...');
@ -109,8 +121,8 @@ const guideFolderChecks = async(number, prFiles, user) => {
labels: currentLabels,
user: { login: username }
} = openPRs[count];
const prFiles = await getFiles(number);
const prFiles = await getFiles(freeCodeCampRepo, number);
if (count > 4000) {
await rateLimiter(2350);
}

View File

@ -5,6 +5,9 @@ To run the script for a specific range,
run `node sweeper.js range startingPrNumber endingPrNumber`
*/
const {
github: { freeCodeCampRepo, defaultBase }
} = require('../lib/config');
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
const { ProcessingLog, rateLimiter } = require('../lib/utils');
const { labeler } = require('../lib/pr-tasks');
@ -13,24 +16,30 @@ const log = new ProcessingLog('add-language-labels');
log.start();
console.log('Curriculum File language labeler started...');
(async() => {
const { totalPRs, firstPR, lastPR } = await getUserInput();
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase
);
const prPropsToGet = ['number', 'labels', 'user'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
let count = 0;
if (openPRs.length) {
console.log('Processing PRs...');
for (let i = 0; i < openPRs.length; i++) {
let { number, labels: currentLabels } = openPRs[i];
const prFiles = await getFiles(number);
const prFiles = await getFiles(freeCodeCampRepo, number);
count++;
const labelsAdded = await labeler(
number,
prFiles,
currentLabels
);
const labelsAdded = await labeler(number, prFiles, currentLabels);
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
log.add(number, { number, labels: labelLogVal });

View File

@ -4,7 +4,10 @@ This is a one-off script to run on all open PRs to add the
"scope: learn" label on it.
*/
const config = require('../config');
const {
github: { freeCodeCampRepo, defaultBase },
oneoff: { productionRun }
} = require('../lib/config');
const { getPRs, getUserInput } = require('../lib/get-prs');
const { addLabels } = require('../lib/pr-tasks');
@ -13,9 +16,19 @@ const { rateLimiter, ProcessingLog } = require('../lib/utils');
const log = new ProcessingLog('all-locally-tested-labels');
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput();
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase
);
const prPropsToGet = ['number', 'labels'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
if (openPRs.length) {
log.start();
@ -36,7 +49,7 @@ const log = new ProcessingLog('all-locally-tested-labels');
if (newLabels.length) {
log.add(number, { number, labels: newLabels });
if (config.oneoff.productionRun) {
if (productionRun) {
addLabels(number, newLabels, log);
await rateLimiter();
}

View File

@ -1,4 +1,8 @@
const config = require('../config');
const {
github: { freeCodeCampRepo, defaultBase },
oneoff: { productionRun }
} = require('../lib/config');
const { closeOpen } = require('../lib/pr-tasks');
const { openJSONFile, ProcessingLog, rateLimiter } = require('../lib/utils');
@ -21,14 +25,14 @@ const getUserInput = async () => {
};
(async () => {
const { prs } = await getUserInput();
const { prs } = await getUserInput(freeCodeCampRepo, defaultBase);
return prs;
})()
.then(async prs => {
for (let { number, errorDesc } of prs) {
if (errorDesc !== 'unknown error') {
log.add(number, { number, closedOpened: true, errorDesc });
if (config.oneoff.productionRun) {
if (productionRun) {
await closeOpen(number);
await rateLimiter(90000);
}

View File

@ -3,16 +3,18 @@ This is a one-off script to find all open PRs which have one of the
console.error descriptions in the failuresToFind.json file.
*/
const { Octokit } = require('@octokit/rest');
const fetch = require('node-fetch');
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
const {
github: { owner, secret, freeCodeCampRepo, defaultBase }
} = require('../lib/config');
const octokit = new Octokit({ auth: secret });
const octokit = require('@octokit/rest')(octokitConfig);
const { getPRs, getUserInput } = require('../lib/get-prs');
const { savePrData, ProcessingLog } = require('../lib/utils');
octokit.authenticate(octokitAuth);
const log = new ProcessingLog('find-failures-script');
const errorsToFind = [
@ -23,9 +25,19 @@ const errorsToFind = [
];
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput();
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase
);
const prPropsToGet = ['number', 'labels', 'head'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
if (openPRs.length) {
savePrData(openPRs, firstPR, lastPR);
@ -46,7 +58,7 @@ const errorsToFind = [
) {
const { data: statuses } = await octokit.repos.listStatusesForRef({
owner,
repo,
repo: freeCodeCampRepo,
ref
});
if (statuses.length) {

View File

@ -9,15 +9,17 @@ Note: If any PR displayed in the console shows "unknown", you will need to rerun
the script again.
*/
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
const octokit = require('@octokit/rest')(octokitConfig);
const {
github: { owner, secret, freeCodeCampRepo, defaultBase }
} = require('../lib/config');
const { Octokit } = require('@octokit/rest');
const octokit = new Octokit({ auth: secret });
const { getPRs, getUserInput } = require('../lib/get-prs');
const { ProcessingLog, rateLimiter } = require('../lib/utils');
const { validLabels } = require('../lib/validation/valid-labels');
octokit.authenticate(octokitAuth);
let languageLabel;
let [languageArg] = process.argv.slice(2);
if (languageArg) {
@ -32,26 +34,49 @@ if (languageLabel) {
const log = new ProcessingLog('unknown-repo-prs-with-merge-conflicts');
log.start();
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase,
'all'
);
const prPropsToGet = ['number', 'labels', 'user', 'head'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
if (openPRs.length) {
let count = 0;
let mergeConflictCount = 0;
for (let i = 0; i < openPRs.length; i++) {
let { labels, number, head: { repo: headRepo } } = openPRs[i];
let {
labels,
number,
head: { repo: headRepo }
} = openPRs[i];
const hasLanguage = languageLabel && labels.some(
({ name }) => languageLabel === name
);
const hasLanguage =
languageLabel && labels.some(({ name }) => languageLabel === name);
if (headRepo === null && (!languageLabel || hasLanguage )) {
let data = await octokit.pullRequests.get({ owner, repo, number });
if (headRepo === null && (!languageLabel || hasLanguage)) {
let data = await octokit.pulls.get({
owner,
repo: freeCodeCampRepo,
number
});
let mergeableState = data.data.mergeable_state;
if (mergeableState === 'unknown') {
await rateLimiter(4000); // allow time to let GitHub determine status
data = await octokit.pullRequests.get({ owner, repo, number });
// allow time to let GitHub determine status
await rateLimiter(4000);
data = await octokit.pulls.get({
owner,
repo: freeCodeCampRepo,
number
});
mergeableState = data.data.mergeable_state;
}
count++;
@ -66,7 +91,9 @@ log.start();
}
}
}
console.log(`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} unknown repos received from GitHub`);
console.log(
`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} unknown repos received from GitHub`
);
} else {
throw 'There were no open PRs received from Github';
}

View File

@ -0,0 +1,236 @@
{
"name": "freecodecampcontributeoneoffscripts",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@octokit/auth-token": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz",
"integrity": "sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q==",
"requires": {
"@octokit/types": "^6.0.0"
}
},
"@octokit/core": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.4.tgz",
"integrity": "sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg==",
"requires": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
"@octokit/request": "^5.4.12",
"@octokit/types": "^6.0.3",
"before-after-hook": "^2.1.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/endpoint": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.10.tgz",
"integrity": "sha512-9+Xef8nT7OKZglfkOMm7IL6VwxXUQyR7DUSU0LH/F7VNqs8vyd7es5pTfz9E7DwUIx7R3pGscxu1EBhYljyu7Q==",
"requires": {
"@octokit/types": "^6.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.8.tgz",
"integrity": "sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA==",
"requires": {
"@octokit/request": "^5.3.0",
"@octokit/types": "^6.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw=="
},
"@octokit/plugin-paginate-rest": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.6.2.tgz",
"integrity": "sha512-3Dy7/YZAwdOaRpGQoNHPeT0VU1fYLpIUdPyvR37IyFLgd6XSij4j9V/xN/+eSjF2KKvmfIulEh9LF1tRPjIiDA==",
"requires": {
"@octokit/types": "^6.0.1"
}
},
"@octokit/plugin-request-log": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz",
"integrity": "sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg=="
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz",
"integrity": "sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA==",
"requires": {
"@octokit/types": "^6.1.0",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "5.4.12",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz",
"integrity": "sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg==",
"requires": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0",
"@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.1",
"once": "^1.4.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.4.tgz",
"integrity": "sha512-LjkSiTbsxIErBiRh5wSZvpZqT4t0/c9+4dOe0PII+6jXR+oj/h66s7E4a/MghV7iT8W9ffoQ5Skoxzs96+gBPA==",
"requires": {
"@octokit/types": "^6.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "18.0.12",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz",
"integrity": "sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw==",
"requires": {
"@octokit/core": "^3.2.3",
"@octokit/plugin-paginate-rest": "^2.6.2",
"@octokit/plugin-request-log": "^1.0.2",
"@octokit/plugin-rest-endpoint-methods": "4.4.1"
}
},
"@octokit/types": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz",
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==",
"requires": {
"@octokit/openapi-types": "^2.0.0",
"@types/node": ">= 8"
}
},
"@types/node": {
"version": "14.14.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz",
"integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ=="
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"before-after-hook": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
},
"cli-progress": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-2.1.1.tgz",
"integrity": "sha512-TSJw3LY9ZRSis7yYzQ7flIdtQMbacd9oYoiFphJhI4SzgmqF0zErO+uNv0lbUjk1L4AGfHQJ4OVYYzW+JV66KA==",
"requires": {
"colors": "^1.1.2",
"string-width": "^2.1.1"
}
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
"requires": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
},
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"requires": {
"inherits": "2.0.3"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

View File

@ -1,7 +1,7 @@
{
"name": "freecodecampcontributeoneoffscripts",
"dependencies": {
"@octokit/rest": "^15.18.0",
"@octokit/rest": "^18.0.12",
"cli-progress": "^2.1.0",
"dedent": "^0.7.0",
"path": "^0.12.7"

View File

@ -1,88 +1,110 @@
/*
This script was created to find all open PRs that have merge
conflicts and then add a the 'status: merge conflict' label to any PR
which does not already have the label.
To run the script for a specific language, call the script with the language
name as the first argument.
Note: It is possible that it could take more than 4 seconds for GitHub to
determine if a PR is mergeable. If that happens, the PR will not be labeled.
*/
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
const octokit = require('@octokit/rest')(octokitConfig);
const { getPRs, getUserInput } = require('../lib/get-prs');
const { ProcessingLog, rateLimiter } = require('../lib/utils');
const { addLabels } = require('../lib/pr-tasks');
const { validLabels } = require('../lib/validation/valid-labels');
octokit.authenticate(octokitAuth);
let languageLabel;
let [languageArg] = process.argv.slice(2);
if (languageArg) {
languageArg = languageArg.toLowerCase();
languageLabel = validLabels[languageArg] ? validLabels[languageArg] : null;
}
if (languageLabel) {
console.log(`finding PRs with label = ${languageLabel}`);
}
const log = new ProcessingLog('prs-with-merge-conflicts');
log.start();
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
const prPropsToGet = ['number', 'labels', 'user'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
if (openPRs.length) {
let count = 0;
let mergeConflictCount = 0;
for (let i = 0; i < openPRs.length; i++) {
let { labels, number } = openPRs[i];
const hasLanguage = languageLabel && labels.some(
({ name }) => languageLabel === name
);
const hasMergeConflictLabel = labels.some(
({ name }) => "status: merge conflict" === name
);
if (!languageLabel || hasLanguage) {
let data = await octokit.pullRequests.get({ owner, repo, number });
let mergeableState = data.data.mergeable_state;
count++;
if (mergeableState === 'unknown') {
await rateLimiter(4000);
data = await octokit.pullRequests.get({ owner, repo, number });
mergeableState = data.data.mergeable_state;
count++;
}
if (mergeableState === 'dirty' && !hasMergeConflictLabel) {
mergeConflictCount++;
addLabels(number, ['status: merge conflict'], log);
await rateLimiter();
}
if (count > 4000) {
await rateLimiter(2350);
}
}
}
console.log(`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} PRs received from GitHub`);
} else {
throw 'There were no open PRs received from Github';
}
})()
.then(async () => {
log.finish();
console.log('Finished finding PRs with merge conflicts');
})
.catch(err => {
log.finish();
console.log(err);
});
/*
This script was created to find all open PRs that have merge
conflicts and then add a the 'status: merge conflict' label to any PR
which does not already have the label.
To run the script for a specific language, call the script with the language
name as the first argument.
Note: It is possible that it could take more than 4 seconds for GitHub to
determine if a PR is mergeable. If that happens, the PR will not be labeled.
*/
const { Octokit } = require('@octokit/rest');
const {
github: { owner, secret, freeCodeCampRepo, defaultBase }
} = require('../lib/config');
const octokit = new Octokit({ auth: secret });
const { getPRs, getUserInput } = require('../lib/get-prs');
const { ProcessingLog, rateLimiter } = require('../lib/utils');
const { addLabels } = require('../lib/pr-tasks');
const { validLabels } = require('../lib/validation/valid-labels');
let languageLabel;
let [languageArg] = process.argv.slice(2);
if (languageArg) {
languageArg = languageArg.toLowerCase();
languageLabel = validLabels[languageArg] ? validLabels[languageArg] : null;
}
if (languageLabel) {
console.log(`finding PRs with label = ${languageLabel}`);
}
const log = new ProcessingLog('prs-with-merge-conflicts');
log.start();
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase,
'all'
);
const prPropsToGet = ['number', 'labels', 'user'];
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
if (openPRs.length) {
let count = 0;
let mergeConflictCount = 0;
for (let i = 0; i < openPRs.length; i++) {
let { labels, number } = openPRs[i];
const hasLanguage =
languageLabel && labels.some(({ name }) => languageLabel === name);
const hasMergeConflictLabel = labels.some(
({ name }) => 'status: merge conflict' === name
);
if (!languageLabel || hasLanguage) {
let data = await octokit.pulls.get({
owner,
repo: freeCodeCampRepo,
number
});
let mergeableState = data.data.mergeable_state;
count++;
if (mergeableState === 'unknown') {
await rateLimiter(4000);
data = await octokit.pulls.get({
owner,
repo: freeCodeCampRepo,
number
});
mergeableState = data.data.mergeable_state;
count++;
}
if (mergeableState === 'dirty' && !hasMergeConflictLabel) {
mergeConflictCount++;
addLabels(number, ['status: merge conflict'], log);
await rateLimiter();
}
if (count > 4000) {
await rateLimiter(2350);
}
}
}
console.log(
`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} PRs received from GitHub`
);
} else {
throw 'There were no open PRs received from Github';
}
})()
.then(async () => {
log.finish();
console.log('Finished finding PRs with merge conflicts');
})
.catch(err => {
log.finish();
console.log(err);
});

View File

@ -10,6 +10,9 @@ To run the script for a specific range,
run `node sweeper.js range startingPrNumber endingPrNumber`
*/
const {
github: { freeCodeCampRepo, defaultBase }
} = require('../lib/config');
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
const { ProcessingLog, rateLimiter } = require('../lib/utils');
const { labeler } = require('../lib/pr-tasks');
@ -19,25 +22,28 @@ const log = new ProcessingLog('sweeper');
log.start();
console.log('Sweeper started...');
(async () => {
const { totalPRs, firstPR, lastPR } = await getUserInput();
const { totalPRs, firstPR, lastPR } = await getUserInput(
freeCodeCampRepo,
defaultBase
);
const prPropsToGet = ['number', 'labels', 'user'];
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
const { openPRs } = await getPRs(
freeCodeCampRepo,
defaultBase,
totalPRs,
firstPR,
lastPR,
prPropsToGet
);
let count = 0;
if (openPRs.length) {
console.log('Processing PRs...');
for (let i = 0; i < openPRs.length; i++) {
let {
number,
labels: currentLabels,
} = openPRs[i];
const prFiles = await getFiles(number);
let { number, labels: currentLabels } = openPRs[i];
const prFiles = await getFiles(freeCodeCampRepo, number);
count++;
const labelsAdded = await labeler(
number,
prFiles,
currentLabels,
);
const labelsAdded = await labeler(number, prFiles, currentLabels);
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
log.add(number, { number, labels: labelLogVal });

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,27 @@
{
"name": "@freecodecamp/contribute",
"name": "@freecodecamp/contributer-tools",
"description": "Tools to help maintain freecodecamp.org's Open Source Codebase on GitHub",
"bin": {
"presolver": "probot/index.js"
},
"scripts": {
"bootstrap": "lerna bootstrap --ci",
"build": "lerna run build",
"postinstall": "npm run bootstrap",
"test": "run-p test:*",
"test:probot": "cd ./probot && npm run test",
"test:client": "cd ./probot/client && npm run test",
"test:client": "cd ./dashboard-app/client && npm run test",
"format": "prettier --write es5 ./**/*.{js,json} && npm run lint",
"lint": "eslint ./**/*.js --fix",
"start": "cd probot && npm start && cd ../",
"start": "cd dashboard-app/server && npm start",
"develop": "run-p develop:*",
"develop:client": "cd ./probot/client && npm run develop",
"develop:server": "cd ./probot/ && npm run develop"
"develop:client": "cd ./dashboard-app/client && npm run develop",
"develop:server": "cd ./dashboard-app/server && npm run develop"
},
"devDependencies": {
"dotenv": "^6.2.0",
"dotenv": "^8.2.0",
"eslint": "^5.9.0",
"eslint-config-freecodecamp": "^1.1.1",
"esm": "^3.0.84",
"joi": "^14.3.1",
"lerna": "^3.5.1",
"mocha": "^5.2.0",
"nodemon": "^1.18.9",
"npm-run-all": "^4.1.5",
"presolver": "file:probot",
"prettier": "^1.15.2"
},
"nodemonConfig": {
@ -38,11 +31,6 @@
"."
]
},
"probot": {
"apps": [
"presolver"
]
},
"dependencies": {
"ajv": "^6.6.1",
"ajv-keywords": "^3.2.0"

View File

@ -0,0 +1,9 @@
GITHUB_USERNAME='camperbot'
GITHUB_ACCESS_TOKEN=
REPOSITORY_OWNER='freeCodeCamp'
REPOSITORY='freeCodeCamp'
DEFAULT_BASE='master'
PORT=3001
MONGO_HOST=mongodb://localhost/contributor-tools
MONGO_PORT=27017
PRODUCTION_RUN=false

View File

@ -1,81 +0,0 @@
## Local Setup
- Follow the steps below to get this running on your local machine
### 1. Copy .env
- Copy the `sample.env` file into `.env`. The command below will do that in the terminal if your CWD(current working directory) is the `contribute` folder.
```bash
cp probot/sample.env probot/.env
```
- If you do not want to populate the database with the freeCodeCamp PR's you can [skip to step 6](#6-start-the-program)
### 2. Update .env
- Put your GitHub username in the `GITHUB_USERNAME` field of the `.env` file
### 3. Obtain GitHub Personal access token
- While on Github, click your profile icon > `Settings`
- Then click `Developer settings` > `Personal access tokens`
- Click `Generate new token`
- On the next page, give the token a name so you know what its for
- Scroll down to the bottom and click `Generate token`
- Copy the token string and paste it into the `GITHUB_ACCESS_TOKEN` field in the `.env` file. Note that you will not be able to see this token string on GitHub again
### 4. Create your own GitHub app
- While on GitHub, click your profile icon > `Settings`
- Then click `developer settings` > `New GitHub app`
- Fill in the `name` field with a name of your choice
- Fill in the `Homepage URL` field with the URL to the GitHub repository for your app. e.g. `https://github.com/username/contribute`
- In a new tab, go to https://smee.io/
- Click `Start a new channel` and copy the URL they give you. You can ignore the rest of the instructions on the `smee.io` page
- Paste the URL you copied into the `WEBHOOK_PROXY_URL` field of the `.env` file and the `Webhook URL` field of your new GitHub app
- Fill in the `Webhook secret` field on the GitHub app with a secret of your choice
- Put the same secret you just used in the `WEBHOOK_SECRET` field of the `.env` file
- Scroll to the bottom of your GitHub app page and click `Create GitHub App`
- On the next page, copy the `App ID` and paste it into the `APP_ID` field of the `.env` file
- Scroll to the bottom of this page and click `Generate Private Key`
- A popup menu will allow you to download the key file. Download it and put it in the `probot` folder
### 5. Run mongoDB
- Make sure a mongoDB instance is running by running the command below in the terminal.
```bash
mongod —dbpath=./database_folder
```
### 6. Start the program
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
```bash
npm install
npm run build
npm run develop
```
- If you skipped steps 2-5, you can ignore the rest
### 7. Update the Database
- Run the command below to populate your local database with PRs from the freeCodeCamp repo. Note that you must have the program running and mongoDB running.
```bash
node probot/server/tools/update-db.js
```
- This will take a while. If it stops running partway through, it's probably a timeout error. Run the command again and it should finish
## Caveats & Notes
### Local Ports when developing locally
Using `npm run develop` will start both the api server and the Create React App(Dashboard) in development mode. The api server runs on port 3001 and the React app runs on port 3000.
### The one-off scripts will error out on actions performed by repository admins
For example, if an admin removes a label from a Pull Request, the script can not add that label back. This is usually because the script is acting on behalf of a non-admin user with write access.
This is usually the case with the use of access tokens for scripts.
### Setting up Cron jobs for Sweeper Scripts
For updating dashboard data we use PM2 like so:
```bash
pm2 start --no-autorestart probot/server/tools/update-db.js --cron "*/10 * * * *"
```
This will start the script in the "no restart" mode and re-run it every 10 minutes.
A useful link to calculate a Cron expression: <https://crontab.guru/every-10-minutes>
### Starting the express server (via probot)
```bash
pm2 start "npm start" --name "contribute-app"
```
**Note:** Start only one instance of this app, you can't have multiple probot apps running. Starting multiple instances will crash the app.

View File

@ -1,4 +0,0 @@
{
"packages": ["probot", "probot/client", "lib", "one-off-scripts"],
"version": "independent"
}

View File

@ -1,30 +0,0 @@
const config = require('../config');
const owner = config.github.owner;
const repo = config.github.repo;
const fccBaseUrl = `https://github.com/${owner}/${repo}/`;
const prBaseUrl = `${fccBaseUrl}pull/`;
const octokitConfig = {
// 0 means no request timeout
timeout: 0,
headers: {
accept: 'application/vnd.github.v3+json',
// v1.2.3 will be current version
'user-agent': 'octokit/rest.js v1.2.3'
},
// custom GitHub Enterprise URL
baseUrl: 'https://api.github.com',
// Node only: advanced request options can be passed as http(s) agent
/* eslint-disable no-undefined */
agent: undefined
/* eslint-enable no-undefined */
};
const octokitAuth = {
type: 'basic',
username: config.github.id,
password: config.github.secret
};
module.exports = { owner, repo, prBaseUrl, octokitConfig, octokitAuth };

View File

@ -1,6 +0,0 @@
module.exports = {
labelPRConflict: {
name: 'PR: potential-conflict',
color: 'c2e0c6'
}
};

View File

@ -1,41 +0,0 @@
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
const octokit = require('@octokit/rest')(octokitConfig);
octokit.authenticate(octokitAuth);
let methodProps = {
owner, repo, state: 'open',
base: 'master', sort: 'created',
page: 1, per_page: 1
};
const getCount = async() => {
const { data: { total_count: count } } = await octokit.search.issues({
q: `repo:${owner}/${repo}+is:open+type:pr+base:master`,
sort: 'created', order: 'asc', page: 1, per_page: 1
})
.catch(err => console.log(err));
return count;
};
const getFirst = async() => {
const response = await octokit.pullRequests.list({
direction: 'asc', ...methodProps
});
return response.data[0].number;
};
const getRange = async() => {
let response = await octokit.pullRequests.list({
direction: 'asc', ...methodProps
});
const firstPR = response.data[0].number;
response = await octokit.pullRequests.list({
direction: 'desc', ...methodProps }
);
const lastPR = response.data[0].number;
return [ firstPR, lastPR ];
};
module.exports = { getCount, getFirst, getRange };

File diff suppressed because it is too large Load Diff

View File

@ -1,318 +0,0 @@
{
"name": "freecodecampcontributeoneoffscripts",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@octokit/rest": {
"version": "15.18.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.18.1.tgz",
"integrity": "sha512-g2tecjp2TEtYV8bKAFvfQtu+W29HM7ektmWmw8zrMy9/XCKDEYRErR2YvvhN9+IxkLC4O3lDqYP4b6WgsL6Utw==",
"requires": {
"before-after-hook": "^1.1.0",
"btoa-lite": "^1.0.0",
"debug": "^3.1.0",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.0",
"lodash": "^4.17.4",
"node-fetch": "^2.1.1",
"universal-user-agent": "^2.0.0",
"url-template": "^2.0.8"
}
},
"agent-base": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"before-after-hook": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-1.3.1.tgz",
"integrity": "sha512-BIjg60OP/sQvG7Q2L9Xkc77gyyFw1B4T73LIfZVQtXbutJinC1+t2HRl4qeR3EWAmY+tA6z9vpRi02q6ZXyluQ=="
},
"btoa-lite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
},
"cli-progress": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-2.1.1.tgz",
"integrity": "sha512-TSJw3LY9ZRSis7yYzQ7flIdtQMbacd9oYoiFphJhI4SzgmqF0zErO+uNv0lbUjk1L4AGfHQJ4OVYYzW+JV66KA==",
"requires": {
"colors": "^1.1.2",
"string-width": "^2.1.1"
}
},
"colors": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz",
"integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg=="
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
"es6-promise": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"execa": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
"integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"http-proxy-agent": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
"requires": {
"agent-base": "4",
"debug": "3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"https-proxy-agent": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
"requires": {
"agent-base": "^4.1.0",
"debug": "^3.1.0"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"macos-release": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.0.0.tgz",
"integrity": "sha512-iCM3ZGeqIzlrH7KxYK+fphlJpCCczyHXc+HhRVbEu9uNTCrzYJjvvtefzeKTCVHd5AP/aD/fzC80JZ4ZP+dQ/A=="
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node-fetch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
"requires": {
"path-key": "^2.0.0"
}
},
"os-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.0.0.tgz",
"integrity": "sha512-7c74tib2FsdFbQ3W+qj8Tyd1R3Z6tuVRNNxXjJcZ4NgjIEQU9N/prVMqcW29XZPXGACqaXN3jq58/6hoaoXH6g==",
"requires": {
"macos-release": "^2.0.0",
"windows-release": "^3.1.0"
}
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
"requires": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"semver": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"requires": {
"shebang-regex": "^1.0.0"
}
},
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
},
"strip-eof": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"universal-user-agent": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.0.2.tgz",
"integrity": "sha512-nOwvHWLH3dBazyuzbECPA5uVFNd7AlgviXRHgR4yf48QqitIvpdncRrxMbZNMpPPEfgz30I9ubd1XmiJiqsTrg==",
"requires": {
"os-name": "^3.0.0"
}
},
"url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE="
},
"util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"requires": {
"inherits": "2.0.3"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"requires": {
"isexe": "^2.0.0"
}
},
"windows-release": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.1.0.tgz",
"integrity": "sha512-hBb7m7acFgQPQc222uEQTmdcGLeBmQLNLFIh0rDk3CwFOBrfjefLzEfEfmpMq8Af/n/GnFf3eYf203FY1PmudA==",
"requires": {
"execa": "^0.10.0"
}
}
}
}

View File

@ -1,139 +0,0 @@
# This is a GitHub App Manifest. These settings will be used by default when
# initially configuring your GitHub App.
#
# NOTE: changing this file will not update your GitHub App settings.
# You must visit github.com/settings/apps/your-app-name to edit them.
#
# Read more about configuring your GitHub App:
# https://probot.github.io/docs/development/#configuring-a-github-app
#
# Read more about GitHub App Manifests:
# https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/
# The list of events the GitHub App subscribes to.
# Uncomment the event names below to enable them.
default_events:
# - check_run
# - check_suite
# - commit_comment
# - create
# - delete
# - deployment
# - deployment_status
# - fork
# - gollum
# - issue_comment
- issues
# - label
# - milestone
# - member
# - membership
# - org_block
# - organization
# - page_build
# - project
# - project_card
# - project_column
# - public
- pull_request
- pull_request_review
- pull_request_review_comment
# - push
# - release
- repository
- repository_import
# - status
# - team
# - team_add
# - watch
# The set of permissions needed by the GitHub App. The format of the object uses
# the permission name for the key (for example, issues) and the access type for
# the value (for example, write).
# Valid values are `read`, `write`, and `none`
default_permissions:
# Repository creation, deletion, settings, teams, and collaborators.
# https://developer.github.com/v3/apps/permissions/#permission-on-administration
administration: read
# Checks on code.
# https://developer.github.com/v3/apps/permissions/#permission-on-checks
checks: read
# Repository contents, commits, branches, downloads, releases, and merges.
# https://developer.github.com/v3/apps/permissions/#permission-on-contents
contents: read
# Deployments and deployment statuses.
# https://developer.github.com/v3/apps/permissions/#permission-on-deployments
# deployments: read
# Issues and related comments, assignees, labels, and milestones.
# https://developer.github.com/v3/apps/permissions/#permission-on-issues
issues: write
# Search repositories, list collaborators, and access repository metadata.
# https://developer.github.com/v3/apps/permissions/#metadata-permissions
metadata: read
# Retrieve Pages statuses, configuration, and builds, as well as create new builds.
# https://developer.github.com/v3/apps/permissions/#permission-on-pages
# pages: read
# Pull requests and related comments, assignees, labels, milestones, and merges.
# https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests
pull_requests: write
# Manage the post-receive hooks for a repository.
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks
repository_hooks: read
# Manage repository projects, columns, and cards.
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects
# repository_projects: read
# Retrieve security vulnerability alerts.
# https://developer.github.com/v4/object/repositoryvulnerabilityalert/
# vulnerability_alerts: read
# Commit statuses.
# https://developer.github.com/v3/apps/permissions/#permission-on-statuses
statuses: read
# Organization members and teams.
# https://developer.github.com/v3/apps/permissions/#permission-on-members
# members: read
# View and manage users blocked by the organization.
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking
# organization_user_blocking: read
# Manage organization projects, columns, and cards.
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects
# organization_projects: read
# Manage team discussions and related comments.
# https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions
# team_discussions: read
# Manage the post-receive hooks for an organization.
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks
organization_hooks: read
# Get notified of, and update, content references.
# https://developer.github.com/v3/apps/permissions/
# organization_administration: read
# The name of the GitHub App. Defaults to the name specified in package.json
# name: My Probot App
# The homepage of your GitHub App.
# url: https://example.com/
# A description of the GitHub App.
# description: A description of my awesome app
# Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app.
# Default: true
# public: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,92 +0,0 @@
const debug = require('debug')('probot:presolver');
const Presolver = require('./server/presolver');
const config = require('../config');
// config should be imported before importing any other file
const mongoose = require('mongoose');
const path = require('path');
async function probotPlugin(robot) {
const events = [
'pull_request.opened',
'pull_request.edited',
'pull_request.synchronize',
'pull_request.reopened',
'pull_request.labeled',
'pull_request.closed'
];
robot.on(events, presolve.bind(null, robot));
// const prOpened = require('./test/payloads/events/pullRequests.opened');
// robot.receive({
// name: 'pull_request.opened',
// payload: prOpened
// });
const redirect = robot.route('/');
redirect.get('/', (req, res) => res.redirect('/home'));
const landingPage = robot.route('/home');
landingPage.use(require('express').static('public'));
const app = robot.route('/dashboard');
const { pareto, pr, search, info } = require('./server/routes');
const staticPath = path.join(__dirname, '.', 'client', 'build');
app.use(require('express').static(staticPath));
app.use((request, response, next) => {
response.header('Access-Control-Allow-Origin', '*');
response.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
response.header('Access-Control-Allow-Methods', 'GET');
next();
});
const landingHtml = path.join(__dirname, './public/index.html');
landingPage.get('/', (req, res) => res.sendFile(landingHtml));
const htmlpath = path.join(__dirname, './client/build/index.html');
app.get('/', (request, response) => response.sendFile(htmlpath));
app.use('/pr', pr);
app.use('/search', search);
app.use('/pareto', pareto);
app.use('/info', info);
app.use(function(err, req, res) {
res.status(err.status || 500).send(err.message);
});
if (mongoose.connection.readyState === 0) {
// connect to mongo db
const mongoUri = config.mongo.host;
const promise = mongoose.connect(
mongoUri, { useNewUrlParser: true }
);
promise
.then(() => {
console.log('MongoDB is connected');
})
.catch(err => {
console.log(err);
console.log('MongoDB connection unsuccessful');
});
}
}
async function presolve(app, context) {
const presolver = forRepository(context);
const pullRequest = getPullRequest(context);
return presolver.presolve(pullRequest);
}
function forRepository(context) {
const config = { ...context.repo({ logger: debug }) };
return new Presolver(context, config);
}
function getPullRequest(context) {
return context.payload.pull_request || context.payload.review.pull_request;
}
module.exports = probotPlugin;

View File

@ -1,145 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="X5tHeKjV-jMLyp4VMoUhW9PAYaOjtPslV250" name="csrf-token">
<title>Contribute to freeCodeCamp | Learn to code with free online courses, programming projects, and interview preparation for developer jobs.</title>
<link href="https://www.freecodecamp.org" rel="canonical">
<meta charset="utf-8">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="X5tHeKjV-jMLyp4VMoUhW9PAYaOjtPslV250" name="csrf-token">
<meta content="Contribute to freeCodeCamp | Learn to code with free online courses, programming projects, and interview preparation for developer jobs." property="og:title">
<meta content="freeCodeCamp" property="og:site_name">
<meta content="on" name="twitter:widgets:csp">
<meta content="d0bc047a482c03c24f1168004c2a216a" name="p:domain_verify">
<meta content="https://www.freecodecamp.org" property="og:url">
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." property="og:description">
<meta content="https://s3.amazonaws.com/freecodecamp/curriculum-diagram-full.jpg" property="og:image">
<meta content="article" property="og:type">
<meta content="https://www.facebook.com/freecodecamp" property="article:publisher">
<meta content="Responsive" property="article:section">
<link href="https://plus.google.com/+Freecodecamp" rel="publisher">
<link href="https://plus.google.com/+Freecodecamp" rel="author">
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." name="description">
<meta content="@freecodecamp" name="twitter:creator">
<meta content="https://www.freecodecamp.org" name="twitter:url">
<meta content="@freecodecamp" name="twitter:site">
<meta content="summary_large_image" name="twitter:card">
<meta content="https://s3.amazonaws.com/freecodecamp/curriculum-diagram-full.jpg" name="twitter:image:src">
<meta content="Learn to code and help nonprofits" name="twitter:title">
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." name="twitter:description">
<meta content="a40ee5d5dba3bb091ad783ebd2b1383f" name="p:domain_verify">
<meta content="#FFFFFF" name="msapplication-TileColor">
<meta content="/" name="msapplication-TileImage">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-144x144.png" rel="android-chrome" sizes="144x144">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-192x192.png" rel="android-chrome" sizes="192x192">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-36x36.png" rel="android-chrome" sizes="36x36">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-48x48.png" rel="android-chrome" sizes="48x48">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-72x72.png" rel="android-chrome" sizes="72x72">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-96x96.png" rel="android-chrome" sizes="96x96">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-manifest.json" rel="android-chrome-manifest">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-114x114.png" rel="apple-touch-icon" sizes="114x114">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-144x144.png" rel="apple-touch-icon" sizes="144x144">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png" rel="apple-touch-icon" sizes="180x180">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-76x76.png" rel="apple-touch-icon" sizes="76x76">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-precomposed.png" rel="apple-touch-icon-precomposed">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon.png" rel="apple-touch-icon">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-16x16.png" rel="favicon" sizes="16x16">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-32x32.png" rel="favicon" sizes="32x32">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-96x96.png" rel="favicon" sizes="96x96">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-144x144.png" rel="mstile" sizes="144x144">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-150x150.png" rel="mstile" sizes="150x150">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-310x310.png" rel="mstile" sizes="310x310">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-310x150.png" rel="mstile" sizes="310x150">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-70x70.png" rel="mstile" sizes="70x70">
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon.ico" rel="favicon">
<link href="//s3.amazonaws.com/freecodecamp/favicons/favicon.ico" rel="shortcut icon">
<link rel='stylesheet' href='https://d33wubrfki0l68.cloudfront.net/css/1d4e5eca38a921b4701240a97afaf40185de57c2/style.css'>
<link href="./style.css" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body class="top-and-bottom-margins">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js">
</script>
<script>
(function(i,s,o,g,r,a,m){ i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-55446531-1', 'auto');
ga('require', 'displayfeatures');
ga('send', 'pageview');
</script><!-- Leave the below lines alone!-->
<nav class="navbar navbar-default navbar-fixed-top nav-height">
<div class="navbar-header">
<a class="navbar-brand" href="https://www.freecodecamp.org"><img alt="learn to code javascript at freeCodeCamp logo" class="img-responsive nav-logo" src="https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg"></a>
</div>
<div class="return-to-free-code-camp">
<ul class="nav navbar-nav navbar-right">
<li class="return-to-free-code-camp">
<a href="/dashboard">Dashboard</a>
</li>
<li class="return-to-free-code-camp">
<a href="https://github.com/freeCodeCamp/freeCodeCamp/" target="_blank" rel="noopener noreferrer">GitHub</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<div class="row flashMessage negative-30">
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"></div>
</div>
</div>
<div class="container">
<div class="row main-content">
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
<h1 class="text-center">Contribute to the Community</h1>
<hr>
<h4>freeCodeCamp is possible thanks to thousands of kind volunteers like you.</p>
<h4>Here are 5 fun quick ways you can help the community right now:</p>
<div class="spacer"></div>
<table class="table link-table">
<tr>
<td class="text-center"><i class="fa fa-question"></i></td>
<td>
<a href="https://forum.freecodecamp.org/" target="_blank" rel="noopener noreferrer">Help campers by answering their questions</a>
</td>
</tr>
<tr>
<td class="text-center"><i class="fa fa-comments"></i></td>
<td>
<a href="https://forum.freecodecamp.org/c/project-feedback/" target="_blank" rel="noopener noreferrer">Give feedback on camper projects</a>
</td>
</tr>
<tr>
<td class="text-center"><i class="fa fa-youtube"></i></td>
<td>
<a href="https://www.youtube.com/timedtext_cs_panel?c=UC8butISFwT-Wl7EV0hUK0BQ&tab=2" rel="noopener noreferrer"target="_blank">Add subtitles to YouTube videos</a>
</td>
</tr>
<tr>
<td class="text-center"><i class="fa fa-book"></i></td>
<td>
<a href="https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md#research-write-and-update-our-guide-articles" rel="noopener noreferrer" target="_blank">Research and write guide articles</a>
</td>
</tr>
<tr>
<td class="text-center"><i class="fa fa-github"></i></td>
<td>
<a href="https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md" rel="noopener noreferrer" target="_blank">Contribute to the codebase</a>
</td>
</tr>
</table>
</div>
</div>
</script>
</body>
</html>

View File

@ -1,11 +0,0 @@
@media (max-width: 991px) {
.main-content {
margin-top: 65px !important;
}
}
.return-to-free-code-camp {
padding: 0!important;
margin: -9px 0!important;
height: 50px!important;
}

Some files were not shown because too many files have changed in this diff Show More