├── .editorconfig ├── .firebaserc ├── .github └── CODEOWNERS ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── failing.png ├── ng-robot-100x100.png ├── ng-robot-28x28.png ├── ng-robot-44x44.png ├── ng-robot.svg └── pending.png ├── database.rules.json ├── docs └── deploy.md ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .gitignore ├── package.json ├── src │ ├── default.ts │ ├── dev.ts │ ├── index.ts │ ├── plugins │ │ ├── common.ts │ │ ├── merge.ts │ │ ├── rerun-circleci.ts │ │ ├── size.ts │ │ ├── task.ts │ │ ├── triage.ts │ │ └── triagePR.ts │ ├── typings.ts │ └── util.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── test ├── common.spec.ts ├── fixtures │ ├── angular-robot.yml │ ├── github-api │ │ ├── get-files.json │ │ ├── get-installation-repositories.json │ │ ├── get-milestone.json │ │ └── get-milestones.json │ ├── installation.created.json │ ├── installation_repositories.added.json │ ├── issues.labeled.json │ ├── issues.milestoned.json │ ├── issues.opened.json │ ├── pr-comments.json │ ├── pull_request.edited.json │ ├── pull_request.labeled.json │ ├── pull_request.opened.json │ ├── pull_request.review_requested.json │ ├── pull_request.synchronize.json │ ├── pull_request_review.submitted.json │ ├── push.json │ └── status.json ├── merge.spec.ts ├── mocks │ ├── database.ts │ ├── firestore.ts │ ├── github.ts │ ├── http.ts │ └── scenarii │ │ └── api.github.com │ │ ├── get-installation-repositories.json │ │ ├── get-installations.json │ │ ├── repo-pull-request-requested-reviewers.json │ │ ├── repo-pull-request-reviews.json │ │ ├── repo-pull-request.json │ │ ├── repo-pull-requests.json │ │ └── repos.json └── triage.spec.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js,ts,css,scss,html}] 14 | charset = utf-8 15 | 16 | # 2 space indentation 17 | [*.{js,ts,css,scss,html}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Matches the exact files either package.json or .travis.yml 22 | [{package.json,.travis.yml,bower.json}] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "angular-robot", 4 | "development": "probot-triage" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence. 3 | * @angular/infrastructure -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | yarn-error.log 4 | 5 | # compiled output 6 | /tmp 7 | /out-tsc 8 | dist 9 | 10 | # dependencies 11 | node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | /.vscode 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | testem.log 37 | /typings 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | 47 | # private files 48 | *.pem 49 | functions/private 50 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "7.7.1" 5 | notifications: 6 | disabled: true 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2017 Google, Inc. http://angular.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Robot 2 | 3 | A Bot built with [probot](https://github.com/probot/probot) to handle multiple tasks on Github 4 | 5 | ## Dev setup 6 | 7 | ``` 8 | # Install dependencies 9 | yarn install 10 | 11 | # Run the bot 12 | npm start 13 | ``` 14 | 15 | 16 | # Usage 17 | This bot is only available for repositories of the [Angular organization](http://github.com/angular/). 18 | See [docs/deploy.md](docs/deploy.md) if you would like to run your own instance. 19 | 20 | ### Adding the bot: 21 | 1. Create `.github/angular-robot.yml` based on the following template 22 | 2. [Configure the Github App](https://github.com/apps/ngbot) 23 | 3. It will start scanning for opened issues and pull requests to monitor 24 | 25 | A [`.github/angular-robot.yml`](test/fixtures/angular-robot.yml) file is required to enable the plugin. The file can be empty, or it can override any of these default settings. 26 | 27 | ### Manual installation 28 | By default the bot will automatically trigger its installation routines when you install it on a new repository. 29 | If for some reason you need to trigger the init manually, you need to change the value `allowInit` to true in the admin / config database and then you can call the "init" function from Firebase functions. Don't forget to set `allowInit` to false after that. 30 | 31 | # Plugins 32 | The bot is designed to run multiple plugins. 33 | 34 | ### Merge plugin: 35 | The merge plugin will monitor pull requests to check whether they are mergeable or not. It will: 36 | - check for conflicts with the base branch and add a comment when it happens 37 | - check for required labels using regexps 38 | - check for forbidden labels using regexps 39 | - check that required statuses are successful 40 | - add a status that is successful when all the checks pass 41 | - monitor the `PR action: merge` label (the name is configurable). If any of the checks is failing it will add a comment to list the reasons 42 | 43 | When you install the bot on a new repository, it will start scanning for opened PRs and monitor them. 44 | 45 | It will **not**: 46 | - add a comment for existing merge labels 47 | - add a comment for conflicts until you push a new commit to the base branch 48 | - add the new merge status until the PR is synchronized (new commit pushed), labeled, unlabeled, or receives another status update 49 | 50 | ### Triage plugin: 51 | The triage plugin will triage issues. It will: 52 | - apply the default milestone when all required labels have been applied (= issue has been triaged) 53 | 54 | 55 | ### Size plugin: 56 | The size plugin will monitor build artifacts from circleci and determine if large chages have occured. It Will 57 | - retrieve artifacts from circleci and save them into the database 58 | - compare artifacts from PRs against ones stored in the database based on the artifact name to determine size increases 59 | - mark a PR as failed if the increase is larger than the amount configured (1000 bytes by default) 60 | - report the size of the largest increase or smallest decrease 61 | -------------------------------------------------------------------------------- /assets/failing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/github-robot/8ab8cebf24c254a21768694fbbf342586284da8d/assets/failing.png -------------------------------------------------------------------------------- /assets/ng-robot-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/github-robot/8ab8cebf24c254a21768694fbbf342586284da8d/assets/ng-robot-100x100.png -------------------------------------------------------------------------------- /assets/ng-robot-28x28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/github-robot/8ab8cebf24c254a21768694fbbf342586284da8d/assets/ng-robot-28x28.png -------------------------------------------------------------------------------- /assets/ng-robot-44x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/github-robot/8ab8cebf24c254a21768694fbbf342586284da8d/assets/ng-robot-44x44.png -------------------------------------------------------------------------------- /assets/pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/github-robot/8ab8cebf24c254a21768694fbbf342586284da8d/assets/pending.png -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": "auth != null" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploying 2 | 3 | If you would like to run your own instance of this app, see the [docs for deployment](https://probot.github.io/docs/deployment/). 4 | 5 | This app requires these **Permissions & events** for the GitHub App: 6 | 7 | - Commit statuses - **Read & Write** 8 | - [x] Check the box for **Status** events 9 | - Issues - **Read & Write** 10 | - [x] Check the box for **Issue comment** events 11 | - [x] Check the box for **Issues** events 12 | - Pull requests - **Read & Write** 13 | - [x] Check the box for **Pull request** events 14 | - [x] Check the box for **Pull request review** events 15 | - [x] Check the box for **Pull request review comment** events 16 | - Repository contents - **Read-only** 17 | - [x] Check the box for **Push** events 18 | 19 | If you want to deploy on Firebase, you'll need to setup the app id, secret and cert as environment parameters: 20 | ```sh 21 | firebase functions:config:set probot.id="[APP_ID]" probot.secret="[SECRET]" probot.cert="[CERT]" 22 | ``` 23 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | }, 9 | "functions": { 10 | "predeploy": [ 11 | "npm --prefix \"functions\" run lint", 12 | "npm --prefix \"functions\" run build" 13 | ], 14 | "source": "functions" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [] 3 | } -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write: if false; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "16" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "@firebase/app-types": "0.3.2", 18 | "firebase-admin": "^11.0.0", 19 | "firebase-functions": "^4.0.2", 20 | "minimatch": "3.0.4", 21 | "node-fetch": "2.2.0", 22 | "probot": "7.4.0" 23 | }, 24 | "devDependencies": { 25 | "tslint": "^6.1.3", 26 | "typescript": "^4.8.4" 27 | }, 28 | "private": true 29 | } 30 | -------------------------------------------------------------------------------- /functions/src/default.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this is the default config that will be used if you don't set the options in your own angular-robot.yml file 3 | */ 4 | export const appConfig: AppConfig = { 5 | size: { 6 | disabled: false, 7 | maxSizeIncrease: 1000, 8 | circleCiStatusName: 'ci/circleci: build', 9 | status: { 10 | disabled: false, 11 | context: "ci/angular: size", 12 | }, 13 | comment: false, 14 | }, 15 | 16 | merge: { 17 | // the status will be added to your pull requests 18 | status: { 19 | // set to true to disable 20 | disabled: false, 21 | // the name of the status 22 | context: "ci/angular: merge status", 23 | // text to show when all checks pass 24 | successText: "All checks passed!", 25 | // text to show when some checks are failing 26 | failureText: "The following checks are failing:" 27 | }, 28 | 29 | g3Status: { 30 | disabled: false, 31 | context: "google3", 32 | pendingDesc: "Googler: run g3sync presubmit {{PRNumber}}", 33 | successDesc: "Does not affect google3", 34 | url: "http://go/angular-g3sync", 35 | include: [ 36 | "LICENSE", 37 | "modules/**", 38 | "packages/**", 39 | ], 40 | exclude: [ 41 | "packages/language-service/**", 42 | "**/.gitignore", 43 | "**/.gitkeep", 44 | "**/tsconfig-build.json", 45 | "**/tsconfig.json", 46 | "**/rollup.config.js", 47 | "**/BUILD.bazel", 48 | "packages/**/test/**", 49 | ] 50 | }, 51 | 52 | // comment that will be added to a PR when there is a conflict, leave empty or set to false to disable 53 | // {{PRAuthor}} will be replaced by the value of the PR author name 54 | mergeConflictComment: `Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges. 55 | Please help to unblock it by resolving these conflicts. Thanks!`, 56 | 57 | // label to monitor 58 | mergeLabel: "PR action: merge", 59 | 60 | // adding any of these labels will also add the merge label 61 | mergeLinkedLabels: ["PR action: merge-assistance"], 62 | 63 | // list of checks that will determine if the merge label can be added 64 | checks: { 65 | // whether the PR shouldn't have a conflict with the base branch 66 | noConflict: true, 67 | // whether the PR should have all reviews completed. 68 | requireReviews: true, 69 | // list of labels that a PR needs to have, checked with a regexp. 70 | requiredLabels: ["cla: yes"], 71 | // list of labels that a PR needs to have, checked only AFTER the merge label has been applied 72 | requiredLabelsWhenMergeReady: ["PR target: *"], 73 | // list of labels that a PR shouldn't have, checked after the required labels with a regexp 74 | forbiddenLabels: ["PR target: TBD", "PR action: cleanup", "PR action: review", "PR state: blocked", "cla: no"], 75 | // list of PR statuses that need to be successful 76 | requiredStatuses: ["continuous-integration/travis-ci/pr", "code-review/pullapprove", "ci/circleci: build", "ci/circleci: lint"], 77 | }, 78 | 79 | // the comment that will be added when the merge label is removed, leave empty or set to false to disable 80 | // {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option 81 | // {{PLACEHOLDER}} will be replaced by the list of failing checks 82 | mergeRemovedComment: `I see that you just added the \`{{MERGE_LABEL}}\` label, but the following checks are still failing: 83 | {{PLACEHOLDER}} 84 | 85 | **If you want your PR to be merged, it has to pass all the CI checks.** 86 | 87 | If you can't get the PR to a green state due to flakes or broken \`main\`, please try rebasing to \`main\` and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.` 88 | }, 89 | 90 | // triage for issues 91 | triage: { 92 | // set to true to disable 93 | disabled: true, 94 | // number of the milestone to apply when the issue has not been triaged yet 95 | needsTriageMilestone: 83, 96 | // number of the milestone to apply when the issue is triaged 97 | defaultMilestone: 82, 98 | // arrays of labels that determine if an issue has been triaged by the caretaker 99 | l1TriageLabels: [["comp: *"]], 100 | // arrays of labels that determine if an issue has been fully triaged 101 | l2TriageLabels: [["type: bug/fix", "severity*", "freq*", "comp: *"], ["type: feature", "comp: *"], ["type: refactor", "comp: *"], ["type: RFC / Discussion / question", "comp: *"]] 102 | }, 103 | 104 | // triage for PRs 105 | triagePR: { 106 | // set to true to disable 107 | disabled: true, 108 | // number of the milestone to apply when the PR has not been triaged yet 109 | needsTriageMilestone: 83, 110 | // number of the milestone to apply when the PR is triaged 111 | defaultMilestone: 82, 112 | // arrays of labels that determine if a PR has been triaged by the caretaker 113 | l1TriageLabels: [["comp: *"]], 114 | // arrays of labels that determine if a PR has been fully triaged 115 | l2TriageLabels: [["type: *", "effort*", "risk*", "comp: *"]] 116 | }, 117 | 118 | rerunCircleCI: { 119 | // set to true to disable 120 | disabled: true, 121 | // the label which when added triggers a rerun of the default CircleCI workflow. 122 | triggerRerunLabel: 'Trigger CircleCI Rerun' 123 | }, 124 | }; 125 | 126 | export interface AppConfig { 127 | merge: MergeConfig; 128 | triage: TriageConfig; 129 | triagePR: TriagePRConfig; 130 | size: SizeConfig; 131 | rerunCircleCI: RerunCircleCIConfig; 132 | } 133 | 134 | export interface MergeConfig { 135 | status: { 136 | disabled: boolean; 137 | context: string; 138 | successText: string; 139 | failureText: string; 140 | }; 141 | g3Status?: { 142 | disabled: boolean; 143 | context: string; 144 | pendingDesc: string; 145 | successDesc: string; 146 | url: string; 147 | include: string[]; 148 | exclude: string[]; 149 | }; 150 | mergeConflictComment: string; 151 | mergeLabel: string; 152 | mergeLinkedLabels?: string[]; 153 | checks: { 154 | noConflict: boolean; 155 | requireReviews: boolean; 156 | requiredLabels: string[]; 157 | requiredLabelsWhenMergeReady: string[]; 158 | forbiddenLabels: string[]; 159 | requiredStatuses: string[]; 160 | }; 161 | mergeRemovedComment: string; 162 | } 163 | 164 | export interface TriageConfig { 165 | disabled: boolean; 166 | needsTriageMilestone: number; 167 | defaultMilestone: number; 168 | l1TriageLabels: string[][]; 169 | l2TriageLabels: string[][]; 170 | /** @deprecated use l2TriageLabels instead */ 171 | triagedLabels?: string[][]; 172 | } 173 | 174 | export interface TriagePRConfig { 175 | disabled: boolean; 176 | needsTriageMilestone: number; 177 | defaultMilestone: number; 178 | l1TriageLabels: string[][]; 179 | l2TriageLabels: string[][]; 180 | } 181 | 182 | export interface SizeConfig { 183 | disabled: boolean; 184 | maxSizeIncrease: number | string; 185 | circleCiStatusName: string; 186 | status: { 187 | disabled: boolean; 188 | context: string; 189 | }; 190 | comment: boolean; 191 | include?: string[]; 192 | exclude?: string[]; 193 | } 194 | 195 | export interface AdminConfig { 196 | allowInit: boolean; 197 | } 198 | 199 | export interface RerunCircleCIConfig { 200 | disabled: boolean; 201 | triggerRerunLabel: string; 202 | } 203 | -------------------------------------------------------------------------------- /functions/src/dev.ts: -------------------------------------------------------------------------------- 1 | import {createProbot, Options} from "probot"; 2 | import {credential, firestore, initializeApp} from "firebase-admin"; 3 | import {consoleStream, loadFirebaseConfig, registerTasks} from "./util"; 4 | 5 | console.warn(`Starting dev mode`); 6 | 7 | const config: Options = require('../private/env.json'); 8 | const appConfig = loadFirebaseConfig("../private/firebase-key.json"); 9 | 10 | initializeApp({ 11 | credential: credential.cert(appConfig), 12 | databaseURL: `https://${appConfig.projectId}.firebaseio.com`, 13 | }); 14 | const store: FirebaseFirestore.Firestore = firestore(); 15 | 16 | // Probot setup 17 | const bot = createProbot(config); 18 | 19 | // disable probot logging 20 | bot.logger.streams.splice(0, 1); 21 | // Use node console as the output stream 22 | bot.logger.addStream(consoleStream(store)); 23 | 24 | // Load plugins 25 | bot.setup([robot => { 26 | registerTasks(robot, store); 27 | }]); 28 | 29 | // Start the bot 30 | bot.start(); 31 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import {config, firestore, https} from 'firebase-functions'; 2 | import {Request, Response} from "express"; 3 | import {createProbot, Options} from "probot"; 4 | import {consoleStream, registerTasks, Tasks} from "./util"; 5 | import {getFirestore} from "firebase-admin/firestore"; 6 | import {initializeApp} from "firebase-admin/app"; 7 | import {DocumentSnapshot} from "firebase-functions/lib/v1/providers/firestore"; 8 | import {EventContext} from "firebase-functions/lib/v1/cloud-functions"; 9 | 10 | let tasks: Tasks; 11 | let probotConfig: Options = config().probot; 12 | 13 | // Check if we are in Firebase or in development 14 | if(!probotConfig) { 15 | // Use dev config 16 | probotConfig = require('../private/env.json'); 17 | } 18 | 19 | // Init Firebase 20 | initializeApp(); 21 | 22 | const store: FirebaseFirestore.Firestore = getFirestore(); 23 | // Create the bot using Firebase's probot config (see Readme.md) 24 | const bot = createProbot(probotConfig); 25 | // disable probot logging 26 | bot.logger.streams.splice(0, 1); 27 | // Use node console as the output stream 28 | bot.logger.addStream(consoleStream(store)); 29 | // Load the merge task to monitor PRs 30 | bot.load(robot => { 31 | tasks = registerTasks(robot, store); 32 | }); 33 | 34 | /** 35 | * Relay Github events to the bot 36 | */ 37 | exports.bot = https.onRequest(async (request: Request, response: Response) => { 38 | const name = request.get('x-github-event') || request.get('X-GitHub-Event'); 39 | const id = request.get('x-github-delivery') || request.get('X-GitHub-Delivery'); 40 | if(name) { 41 | try { 42 | await bot.receive({ 43 | name, 44 | id, 45 | payload: request.body, 46 | protocol: 'https', 47 | host: request.hostname, 48 | url: request.url 49 | }); 50 | response.send({ 51 | statusCode: 200, 52 | body: JSON.stringify({ 53 | message: 'Executed' 54 | }) 55 | }); 56 | } catch(err) { 57 | console.error(err); 58 | response.sendStatus(500); 59 | } 60 | } else { 61 | console.error(request); 62 | response.sendStatus(400); 63 | } 64 | }); 65 | 66 | /** 67 | * Manually trigger init for all repositories, you shouldn't need to use that unless you clean the database 68 | */ 69 | exports.init = https.onRequest(async (request: Request, response: Response) => { 70 | try { 71 | await tasks.commonTask.manualInit().catch(err => { 72 | console.error(err); 73 | }); 74 | response.send({ 75 | statusCode: 200, 76 | body: JSON.stringify({ 77 | message: 'Common init function started' 78 | }) 79 | }); 80 | } catch(err) { 81 | console.error(err); 82 | response.sendStatus(500); 83 | } 84 | }); 85 | 86 | /** 87 | * Manually trigger init to triage issues, you shouldn't need to use that unless you clean the database 88 | */ 89 | exports.initTriage = https.onRequest(async (request: Request, response: Response) => { 90 | try { 91 | await tasks.triageTask.manualInit().catch(err => { 92 | console.error(err); 93 | }); 94 | response.send({ 95 | statusCode: 200, 96 | body: JSON.stringify({ 97 | message: 'Init triage issues function started' 98 | }) 99 | }); 100 | } catch(err) { 101 | console.error(err); 102 | response.sendStatus(500); 103 | } 104 | }); 105 | 106 | /** 107 | * Manually trigger init to triage PR, you shouldn't need to use that unless you clean the database 108 | */ 109 | exports.initTriagePR = https.onRequest(async (request: Request, response: Response) => { 110 | try { 111 | await tasks.triagePRTask.manualInit().catch(err => { 112 | console.error(err); 113 | }); 114 | response.send({ 115 | statusCode: 200, 116 | body: JSON.stringify({ 117 | message: 'Init triage PRs function started' 118 | }) 119 | }); 120 | } catch(err) { 121 | console.error(err); 122 | response.sendStatus(500); 123 | } 124 | }); 125 | 126 | /** 127 | * Init the PRs of a repository, triggered by an insertion in the "repositories" table 128 | */ 129 | exports.initRepoPRs = firestore.document('repositories/{id}').onCreate((snapshot: DocumentSnapshot, context: EventContext) => { 130 | const data = snapshot.data(); 131 | return tasks.commonTask.triggeredInit(data).catch(err => { 132 | console.error(err); 133 | }); 134 | }); 135 | 136 | /** 137 | * Delete the cache for config files. 138 | * This can be used to force a refresh if the cached config is wrong (if we missed the push event for example). 139 | */ 140 | exports.deleteCachedConfigs = https.onRequest(async (request: Request, response: Response) => { 141 | try { 142 | await tasks.commonTask.deleteCachedConfigs().catch(err => { 143 | console.error(err); 144 | }); 145 | response.send({ 146 | statusCode: 200, 147 | body: JSON.stringify({ 148 | message: 'All cached configurations have been deleted' 149 | }) 150 | }); 151 | } catch(err) { 152 | console.error(err); 153 | response.sendStatus(500); 154 | } 155 | }); 156 | -------------------------------------------------------------------------------- /functions/src/plugins/common.ts: -------------------------------------------------------------------------------- 1 | import {Application, Context} from "probot"; 2 | import {WebhookPayloadPush} from '@octokit/webhooks'; 3 | import Github from '@octokit/rest'; 4 | import minimatch from "minimatch"; 5 | import {AdminConfig} from "../default"; 6 | import {CONFIG_FILE, Task} from "./task"; 7 | import {GitHubAPI} from "probot/lib/github"; 8 | import {firestore} from "firebase-admin"; 9 | import GithubGQL, {Commit} from "../typings"; 10 | 11 | export class CommonTask extends Task { 12 | constructor(robot: Application, db: FirebaseFirestore.Firestore) { 13 | super(robot, db); 14 | // App installations on a new repository 15 | this.dispatch([ 16 | 'installation.created', 17 | 'installation_repositories.added' 18 | ], this.installInit.bind(this)); 19 | 20 | this.dispatch('push', this.onPush.bind(this)); 21 | } 22 | 23 | /** 24 | * Init all existing repositories 25 | * Manual call 26 | */ 27 | async manualInit(): Promise { 28 | const adminConfig = await this.admin.doc('config').get(); 29 | if(adminConfig.exists && (adminConfig.data()).allowInit) { 30 | const github = await this.robot.auth(); 31 | const installations = await github.paginate(github.apps.listInstallations({}), pages => (pages as any as Github.AnyResponse).data); 32 | await Promise.all(installations.map(async installation => { 33 | const authGithub = await this.robot.auth(installation.id); 34 | const repositories = await authGithub.apps.listRepos({}); 35 | await Promise.all(repositories.data.repositories.map(async (repository: Github.AppsListReposResponseRepositoriesItem) => { 36 | await this.repositories.doc(repository.id.toString()).set({ 37 | id: repository.id, 38 | name: repository.name, 39 | full_name: repository.full_name, 40 | installationId: installation.id 41 | }).catch(err => { 42 | this.robot.log.error(err); 43 | throw err; 44 | }); 45 | })); 46 | })); 47 | } else { 48 | this.robot.log.error(`Manual init is disabled: the value of allowInit is set to false in the admin config database`); 49 | } 50 | } 51 | 52 | /** 53 | * Init a single repository 54 | * Triggered by Firebase when there is an insertion into the Firebase collection "repositories" 55 | */ 56 | async triggeredInit(data: firestore.DocumentData): Promise { 57 | const repository = data as Repository & { installationId: number }; 58 | const authGithub = await this.robot.auth(repository.installationId); 59 | return this.init(authGithub, [repository]); 60 | } 61 | 62 | /** 63 | * Updates the database with existing PRs when the bot is installed on a new server 64 | * Triggered by event 65 | */ 66 | async installInit(context: Context): Promise { 67 | let repositories: Repository[]; 68 | switch(context.name) { 69 | case 'installation': 70 | repositories = context.payload.repositories; 71 | break; 72 | case 'installation_repositories': 73 | repositories = context.payload.repositories_added; 74 | break; 75 | } 76 | 77 | await Promise.all(repositories.map(async repository => { 78 | await this.repositories.doc(repository.id.toString()).set({ 79 | ...repository, 80 | installationId: context.payload.installation.id 81 | }).catch(err => { 82 | this.robot.log.error(err); 83 | throw err; 84 | }); 85 | })); 86 | } 87 | 88 | /** 89 | * Updates the PRs in Firebase for a list of repositories 90 | */ 91 | async init(github: GitHubAPI, repositories: Repository[]): Promise { 92 | await Promise.all(repositories.map(async repository => { 93 | this.robot.log(`Starting init for repository "${repository.full_name}"`); 94 | const [owner, repo] = repository.full_name.split('/'); 95 | 96 | const dbPRSnapshots = await this.pullRequests 97 | .where('repository', '==', repository.id) 98 | .where('state', '==', 'open') 99 | .get(); 100 | 101 | // list of existing opened PRs in the db 102 | const dbPRs = dbPRSnapshots.docs.map(doc => doc.id); 103 | 104 | const ghPRs = await github.paginate(github.pullRequests.list({ 105 | owner, 106 | repo, 107 | state: 'open', 108 | per_page: 100 109 | }), pages => (pages as any as Github.AnyResponse).data) as Github.PullRequestsListResponse; 110 | 111 | ghPRs.forEach(async pr => { 112 | const index = dbPRs.indexOf(pr.id.toString()); 113 | if(index !== -1) { 114 | dbPRs.splice(index, 1); 115 | } 116 | }); 117 | 118 | // update the state of all PRs that are no longer opened 119 | if(dbPRs.length > 0) { 120 | const batch = this.db.batch(); 121 | dbPRs.forEach(async id => { 122 | batch.set(this.pullRequests.doc(id.toString()), {state: 'closed'}, {merge: true}); 123 | }); 124 | batch.commit().catch(err => { 125 | this.robot.log.error(err); 126 | throw err; 127 | }); 128 | } 129 | 130 | // add/update opened PRs 131 | return Promise.all(ghPRs.map(pr => github.pullRequests.get({number: pr.number, owner, repo}) 132 | .then(res => this.updateDbPR(github, owner, repo, pr.number, repository.id, res.data)))); 133 | })); 134 | } 135 | 136 | async deleteCachedConfigs(): Promise { 137 | const query = await this.config.get(); 138 | 139 | // When there are no documents left, we are done 140 | if (query.size === 0) { 141 | return; 142 | } 143 | 144 | // Delete documents in a batch 145 | const batch = this.db.batch(); 146 | query.docs.forEach((doc) => { 147 | batch.delete(doc.ref); 148 | }); 149 | 150 | await batch.commit(); 151 | return; 152 | } 153 | 154 | /** 155 | * Update the config file when there is a push to the default branch. 156 | */ 157 | async onPush(context: Context): Promise { 158 | const payload = context.payload as WebhookPayloadPush; 159 | const refParts = payload.ref.split('/'); 160 | const branchName = refParts[refParts.length - 1]; 161 | const defaultBranch = payload.repository.default_branch; 162 | 163 | // If a config update lands in the default branch, we refresh the stored configuration. 164 | if (branchName === defaultBranch) { 165 | const commits = context.payload.commits; 166 | const updatedConfig = commits.some((commit: Commit) => commit.modified.includes(`.github/${CONFIG_FILE}`)); 167 | if(updatedConfig) { 168 | await this.refreshConfig(context); 169 | } 170 | } 171 | return; 172 | } 173 | } 174 | 175 | /** 176 | * Gets the PR labels from Github. 177 | * Uses GraphQL API. 178 | */ 179 | export async function getGhPRLabels(github: GitHubAPI, owner: string, repo: string, number: number): Promise { 180 | return (await queryPR(github, ` 181 | labels(first: 50) { 182 | nodes { 183 | id, 184 | url, 185 | name, 186 | description, 187 | color, 188 | } 189 | } 190 | `, 191 | { 192 | owner, 193 | repo, 194 | number 195 | })).labels.nodes; 196 | } 197 | 198 | export function getLabelsNames(labels: Github.IssuesGetResponseLabelsItem[] | string[] | GithubGQL.Labels['nodes']): string[] { 199 | if(typeof labels[0] !== 'string') { 200 | labels = (labels as Github.IssuesGetResponseLabelsItem[]).map(label => label.name); 201 | } 202 | return labels as string[]; 203 | } 204 | 205 | /** 206 | * Adds a comment on a PR 207 | */ 208 | export async function addComment(github: Github, owner: string, repo: string, number: number, body: string): Promise { 209 | return github.issues.createComment({ 210 | owner, 211 | repo, 212 | number, 213 | body 214 | }); 215 | } 216 | 217 | export async function addLabels(github: Github, owner: string, repo: string, number: number, labels: string[]): Promise { 218 | return github.issues.addLabels({ 219 | owner, 220 | repo, 221 | number, 222 | labels 223 | }); 224 | } 225 | 226 | interface Repository { 227 | id: number; 228 | name: string; 229 | full_name: string; 230 | } 231 | 232 | /** 233 | * Returns true if any of the names match any of the patterns 234 | * It ignores any pattern match that is also matching a negPattern 235 | */ 236 | export function matchAny(names: string[], patterns: (string | RegExp)[], negPatterns: (string | RegExp)[] = []): boolean { 237 | return names.some(name => 238 | patterns.some(pattern => 239 | !!name.match(new RegExp(pattern)) && !negPatterns.some(negPattern => 240 | !!name.match(new RegExp(negPattern)) 241 | ) 242 | ) 243 | ); 244 | } 245 | 246 | /** 247 | * Same as matchAny, but for files, takes paths into account 248 | * Returns true if any of the names match any of the patterns 249 | * It ignores any pattern match that is also matching a negPattern 250 | */ 251 | export function matchAnyFile(names: string[], patterns: string[], negPatterns: string[] = []): boolean { 252 | return names.some(name => 253 | patterns.some(pattern => 254 | minimatch(name, pattern) && !negPatterns.some(negPattern => 255 | minimatch(name, negPattern) 256 | ) 257 | ) 258 | ); 259 | } 260 | 261 | /** 262 | * Returns true if some of the names match all of one of the patterns array 263 | * e.g.: [a, b, c] match the first pattern of [[a, b], [a, d]], but [a, b, c] doesn't match [[a, d], [b, e]] 264 | */ 265 | export function matchAllOfAny(names: string[], patternsArray: string[][]): boolean { 266 | return patternsArray 267 | // is one of the patterns array 100% present? 268 | .some((patterns: string[]) => patterns 269 | // for this array of patterns, are they all matching one of the current names? 270 | .map(pattern => names 271 | // is this name matching one of the current label 272 | // we replace "/" by "*" because we are matching labels not files 273 | .some(name => !!name.match(new RegExp(pattern))) 274 | ) 275 | // are they all matching or is at least one of them not a match 276 | .reduce((previous: boolean, current: boolean) => previous && current) 277 | ); 278 | } 279 | 280 | export async function queryPR(github: GitHubAPI, query: string, params: { [key: string]: any, owner: string, repo: string, number: number }): Promise { 281 | return (await github.query(`query($owner: String!, $repo: String!, $number: Int!) { 282 | repository(owner: $owner, name: $repo) { 283 | pullRequest(number: $number) { 284 | ${query} 285 | } 286 | } 287 | }`, params)).repository.pullRequest; 288 | } 289 | 290 | export async function queryIssue(github: GitHubAPI, query: string, params: { [key: string]: any, owner: string, repo: string, number: number }): Promise { 291 | return (await github.query(`query($owner: String!, $repo: String!, $number: Int!) { 292 | repository(owner: $owner, name: $repo) { 293 | issue(number: $number) { 294 | ${query} 295 | } 296 | } 297 | }`, params)).repository.issue; 298 | } 299 | 300 | 301 | export async function queryNode(github: GitHubAPI, query: string, params: { [key: string]: any, owner: string, repo: string, number: number, nodeId: string }): Promise { 302 | return (await github.query(`query($owner: String!, $repo: String!, $number: Int!) { 303 | node(id: $nodeId) { 304 | ${query} 305 | } 306 | }`, params)).node; 307 | } 308 | -------------------------------------------------------------------------------- /functions/src/plugins/rerun-circleci.ts: -------------------------------------------------------------------------------- 1 | import {config as firebaseConfig} from 'firebase-functions'; 2 | import {Application, Context} from "probot"; 3 | import {Task} from "./task"; 4 | import {RerunCircleCIConfig} from "../default"; 5 | import Github from '@octokit/rest'; 6 | import fetch from "node-fetch"; 7 | 8 | let circleCIConfig = firebaseConfig().circleci; 9 | 10 | // Check if we are in Firebase or in development 11 | if(!circleCIConfig) { 12 | // Use dev config 13 | circleCIConfig = require('../../private/circle-ci.json'); 14 | } 15 | 16 | const CIRCLE_CI_TOKEN = circleCIConfig.token; 17 | 18 | export class RerunCircleCITask extends Task { 19 | constructor(robot: Application, db: FirebaseFirestore.Firestore) { 20 | super(robot, db); 21 | 22 | // Dispatch when a label is added to a pull request. 23 | this.dispatch([ 24 | 'pull_request.labeled', 25 | ], this.checkRerunCircleCI.bind(this)); 26 | } 27 | 28 | /** Determines if a circle rerun should occur. */ 29 | async checkRerunCircleCI(context: Context): Promise { 30 | const config = await this.getConfig(context); 31 | if (config.disabled) { 32 | return; 33 | } 34 | 35 | if (context.payload.label) { 36 | const label: Github.IssuesGetLabelResponse = context.payload.label; 37 | if (label.name === config.triggerRerunLabel) { 38 | await this.triggerCircleCIRerun(context); 39 | } 40 | } 41 | } 42 | 43 | /** Triggers a rerun of the default CircleCI workflow and then removed the triggering label. */ 44 | async triggerCircleCIRerun(context: Context) { 45 | const config = await this.getConfig(context); 46 | if (config.disabled) { 47 | return; 48 | } 49 | 50 | const pullRequest: Github.PullRequestsGetResponse = context.payload.pull_request; 51 | const sender: Github.PullRequestsGetResponseUser = context.payload.sender; 52 | const {owner, repo} = context.repo(); 53 | const circleCiUrl = `https://circleci.com/api/v2/project/gh/${owner}/${repo}/pipeline?circle-token=${CIRCLE_CI_TOKEN}`; 54 | try { 55 | const response = await fetch(circleCiUrl, { 56 | method: 'POST', 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | body: JSON.stringify({ 61 | branch: `pull/${pullRequest.number}/head`, 62 | }) 63 | }); 64 | // Properly handled failures in the CircleCI requests are returned with an HTTP response code 65 | // of 200 and json response with a `:message` key mapping to the failure message. If 66 | // `:message` is not defined, the API request was successful. 67 | const errMessage = (await response.json())[':message']; 68 | if (errMessage) { 69 | throw Error(errMessage); 70 | } 71 | } catch (err) { 72 | const error: TypeError = err; 73 | context.github.issues.createComment({ 74 | body: `@${sender.login} the CircleCI rerun you requested failed. See details below: 75 | 76 | \`\`\` 77 | ${error.message} 78 | \`\`\``, 79 | number: pullRequest.number, 80 | owner: owner, 81 | repo: repo, 82 | }).catch(err => { 83 | throw err; 84 | }); 85 | } 86 | await context.github.issues.removeLabel({ 87 | name: config.triggerRerunLabel, 88 | number: pullRequest.number, 89 | owner: owner, 90 | repo: repo 91 | }); 92 | } 93 | 94 | /** 95 | * Gets the config for the merge plugin from Github or uses default if necessary 96 | */ 97 | async getConfig(context: Context): Promise { 98 | const repositoryConfig = await this.getAppConfig(context); 99 | return repositoryConfig.rerunCircleCI; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /functions/src/plugins/size.ts: -------------------------------------------------------------------------------- 1 | import fetch, {HeadersInit} from 'node-fetch'; 2 | import {Application, Context} from "probot"; 3 | import {Task} from "./task"; 4 | import {SizeConfig, appConfig as defaultAppConfig} from "../default"; 5 | import {STATUS_STATE} from "../typings"; 6 | import Github from '@octokit/rest'; 7 | import {config as firebaseFunctionConfig} from 'firebase-functions'; 8 | import {DocumentReference} from 'firebase-admin/firestore'; 9 | 10 | export interface CircleCiArtifact { 11 | path: string; 12 | node_index: number; 13 | url: string; 14 | } 15 | 16 | export interface BuildArtifact { 17 | path: string; 18 | url: string; 19 | size: number; 20 | projectName: string; 21 | context: string; 22 | filename: string; 23 | } 24 | 25 | export interface BuildArtifactDiff { 26 | current: BuildArtifact; 27 | baseline: BuildArtifact; 28 | delta: number; 29 | failed: boolean; 30 | } 31 | 32 | const byteUnits = 'KMGT'; 33 | const byteBase = 1024; 34 | 35 | function formatBytes(value: number): string { 36 | const i = Math.min(Math.trunc(Math.log(Math.abs(value)) / Math.log(byteBase)), byteUnits.length); 37 | if(i === 0) { 38 | return value + ' bytes'; 39 | } 40 | 41 | return (value / byteBase ** i).toFixed(2) + byteUnits[i - 1] + 'B'; 42 | } 43 | 44 | export class SizeTask extends Task { 45 | constructor(robot: Application, db: FirebaseFirestore.Firestore) { 46 | super(robot, db); 47 | 48 | this.dispatch('status', (context) => this.checkSize(context)); 49 | } 50 | 51 | async checkSize(context: Context): Promise { 52 | const appConfig = await this.getAppConfig(context); 53 | 54 | if(!appConfig.size || appConfig.size.disabled) { 55 | return; 56 | } 57 | 58 | const config: SizeConfig = { 59 | ...defaultAppConfig.size, 60 | ...appConfig.size, 61 | status: { ...defaultAppConfig.size.status, ...appConfig.size.status }, 62 | }; 63 | 64 | const statusEvent = context.payload; 65 | 66 | // only check on PRs the status has that artifacts 67 | if(statusEvent.context !== config.circleCiStatusName) { 68 | return; 69 | } 70 | 71 | if(statusEvent.state === STATUS_STATE.Pending) { 72 | await this.setStatus( 73 | STATUS_STATE.Pending, 74 | `Waiting for "${config.circleCiStatusName}"...`, 75 | config.status.context, 76 | context, 77 | ); 78 | 79 | return; 80 | } else if(statusEvent.state === STATUS_STATE.Failure) { 81 | await this.setStatus( 82 | STATUS_STATE.Error, 83 | `Unable to calculate sizes. Failure: "${config.circleCiStatusName}"`, 84 | config.status.context, 85 | context, 86 | ); 87 | 88 | return; 89 | } 90 | 91 | const {owner, repo} = context.repo(); 92 | const buildNumber = this.getBuildNumberFromCircleCIUrl(statusEvent.target_url); 93 | 94 | let newArtifacts; 95 | try { 96 | newArtifacts = await this.getCircleCIArtifacts(owner, repo, buildNumber, config.exclude, config.include); 97 | } catch(e) { 98 | this.logError('CircleCI Artifact retrieval error: ' + e.message); 99 | await this.setStatus( 100 | STATUS_STATE.Error, 101 | `Unable to retrieve artifacts from "${config.circleCiStatusName}".`, 102 | config.status.context, 103 | context, 104 | ); 105 | 106 | return; 107 | } 108 | 109 | const pr = await this.findPrBySha(statusEvent.sha, statusEvent.repository.id); 110 | if(!pr) { 111 | // this status doesn't have a PR therefore it's probably a commit to a branch 112 | // so we want to store any changes from that commit 113 | await this.upsertNewArtifacts(context, newArtifacts); 114 | 115 | await this.setStatus( 116 | STATUS_STATE.Success, 117 | `Baseline saved for ${statusEvent.sha}`, 118 | config.status.context, 119 | context, 120 | ); 121 | 122 | return; 123 | } 124 | 125 | this.logDebug(`[size] Processing PR: ${pr.title}`); 126 | 127 | // set to pending since we are going to do a full run through 128 | await this.setStatus( 129 | STATUS_STATE.Pending, 130 | 'Calculating artifact sizes...', 131 | config.status.context, 132 | context, 133 | ); 134 | 135 | const targetBranchArtifacts = await this.getTargetBranchArtifacts(pr); 136 | 137 | if(targetBranchArtifacts.length === 0) { 138 | await this.setStatus( 139 | STATUS_STATE.Success, 140 | `No baseline available for ${pr.base.ref} / ${pr.base.sha}`, 141 | config.status.context, 142 | context, 143 | ); 144 | 145 | return; 146 | } 147 | 148 | const comparisons = this.generateArtifactComparisons(targetBranchArtifacts, newArtifacts, config); 149 | const largestIncrease = comparisons.length > 0 ? comparisons[0] : null; 150 | const failure = largestIncrease && largestIncrease.failed; 151 | 152 | let description; 153 | if(!largestIncrease) { 154 | description = 'No matching artifacts to compare.'; 155 | } else if(largestIncrease.delta === 0) { 156 | description = 'No size change against base branch.'; 157 | } else { 158 | const direction = largestIncrease.delta > 0 ? 'increased' : 'decreased'; 159 | const formattedBytes = formatBytes(Math.abs(largestIncrease.delta)); 160 | description = `${largestIncrease.current.path} ${direction} by ${formattedBytes}.`; 161 | 162 | 163 | // Add comment if enabled 164 | if (config.comment) { 165 | let body = '|| Artifact | Baseline | Current | Change |\n|-|-|-|-|-|\n'; 166 | 167 | for (const comparison of comparisons) { 168 | const emoji = comparison.delta <= 0 ? ':white_check_mark:' : ':grey_exclamation:'; 169 | body += `| ${comparison.failed ? ':x:' : emoji}|${comparison.baseline.path}`; 170 | body += `|[${formatBytes(comparison.baseline.size)}](${comparison.baseline.url})`; 171 | body += `|[${formatBytes(comparison.current.size)}](${comparison.current.url})`; 172 | body += `|${comparison.delta > 0 ? '+' : ''}${formatBytes(comparison.delta)}|`; 173 | } 174 | 175 | try { 176 | const prDoc = await this.pullRequests.doc(pr.id.toString()).get(); 177 | let commentId = prDoc.exists ? prDoc.data().sizeCheckComment : undefined; 178 | 179 | if (commentId !== undefined) { 180 | try { 181 | await context.github.issues.updateComment({ 182 | owner, 183 | repo, 184 | comment_id: commentId, 185 | body, 186 | }); 187 | } catch { 188 | // Comment may have been deleted 189 | commentId = undefined; 190 | } 191 | } 192 | 193 | if (commentId === undefined) { 194 | const response = await context.github.issues.createComment({ 195 | owner, 196 | repo, 197 | number: pr.number, 198 | body, 199 | }); 200 | 201 | await prDoc.ref.update({ sizeCheckComment: response.data.id }); 202 | } 203 | } catch (e) { 204 | this.logError(`Unable to add size comment [${e.message}]`); 205 | } 206 | } 207 | } 208 | 209 | return this.setStatus( 210 | failure ? STATUS_STATE.Failure : STATUS_STATE.Success, 211 | description, 212 | config.status.context, 213 | context, 214 | ); 215 | } 216 | 217 | /** 218 | * 219 | * Insert or updates the artifacts for a status event 220 | * 221 | * @param context Must be from a "Status" github event 222 | * @param artifacts 223 | */ 224 | async upsertNewArtifacts(context: Context, artifacts: BuildArtifact[]): Promise { 225 | this.logDebug(`[size] Storing artifacts for: ${context.payload.sha}, on branches [${context.payload.branches.map((b: Github.ReposListBranchesResponseItem) => b.commit.url).join(', ')}]`); 226 | 227 | const updatedAt = context.payload.updated_at; 228 | const branch = context.payload.branches 229 | .find((b: Github.ReposListBranchesResponseItem) => b.commit.sha === context.payload.commit.sha); 230 | const sizeArtifacts = this.repositories 231 | .doc(context.payload.repository.id.toString()) 232 | .collection('sizeArtifacts'); 233 | 234 | // Generate Document IDs from sha and artifact path 235 | const artifactDocs = artifacts.map(a => sizeArtifacts.doc( 236 | Buffer.from(context.payload.sha + a.path).toString('base64'), 237 | )); 238 | 239 | return sizeArtifacts.firestore.runTransaction(async transaction => { 240 | const results = await transaction.getAll(...artifactDocs as DocumentReference[]); 241 | 242 | for(let i = 0; i < results.length; ++i) { 243 | if(results[i].exists) { 244 | if(results[i].data().updatedAt < updatedAt) { 245 | transaction.update(results[i].ref, { 246 | ...artifacts[i], 247 | sha: context.payload.commit.sha, 248 | updatedAt: context.payload.updated_at, 249 | ...(branch ? {branch: branch.name} : {}), 250 | }); 251 | } 252 | } else { 253 | transaction.create(results[i].ref, { 254 | ...artifacts[i], 255 | sha: context.payload.commit.sha, 256 | updatedAt: context.payload.updated_at, 257 | ...(branch ? {branch: branch.name} : {}), 258 | }); 259 | } 260 | } 261 | }); 262 | } 263 | 264 | /** 265 | * 266 | * Parses a circleci build url for the build number 267 | * 268 | * @param url circleci build url, retrieved from target_event in a github "Status" event context 269 | */ 270 | getBuildNumberFromCircleCIUrl(url: string): number { 271 | const parts = url.split('/'); 272 | 273 | if(parts[2] === 'circleci.com' && parts[3] === 'gh') { 274 | return Number(parts[6].split('?')[0]); 275 | } else { 276 | throw new Error('incorrect circleci path'); 277 | } 278 | } 279 | 280 | parseBytes(input: number | string): [number, boolean] { 281 | if (typeof input === 'number') { 282 | return [input, false]; 283 | } 284 | 285 | const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/); 286 | if (!matches) { 287 | return [NaN, false]; 288 | } 289 | 290 | let value = Number(matches[1]); 291 | switch (matches[2] && matches[2].toLowerCase()) { 292 | case '%': 293 | return [value / 100, true]; 294 | case 'kb': 295 | value *= 1024; 296 | break; 297 | case 'mb': 298 | value *= 1024 ** 2; 299 | break; 300 | case 'gb': 301 | value *= 1024 ** 3; 302 | break; 303 | } 304 | 305 | return [value, false]; 306 | } 307 | 308 | generateArtifactComparisons(oldArtifacts: BuildArtifact[], newArtifacts: BuildArtifact[], config: SizeConfig) { 309 | const baselines = new Map(oldArtifacts.map<[string, BuildArtifact]>(a => [a.path, a])); 310 | const [threshold, percentage] = this.parseBytes(config.maxSizeIncrease); 311 | 312 | if (isNaN(threshold)) { 313 | this.logError('Invalid size configuration'); 314 | return []; 315 | } 316 | 317 | const comparisons: BuildArtifactDiff[] = []; 318 | for (const current of newArtifacts) { 319 | const baseline = baselines.get(current.path); 320 | 321 | if (!baseline) { 322 | continue; 323 | } 324 | 325 | const delta = current.size - baseline.size; 326 | comparisons.push({ 327 | current, 328 | baseline, 329 | delta, 330 | failed: delta > (percentage ? threshold * baseline.size : threshold), 331 | }); 332 | } 333 | 334 | comparisons.sort((a, b) => b.delta - a.delta); 335 | 336 | return comparisons; 337 | } 338 | 339 | /** 340 | * Finds the target branch of a PR then retrieves the artifacts at the for the HEAD of that branch 341 | */ 342 | async getTargetBranchArtifacts(prPayload: Github.PullRequestsGetResponse): Promise { 343 | const targetBranch = prPayload.base; 344 | this.logDebug(`[size] Fetching target branch artifacts for ${targetBranch.ref}/${targetBranch.sha}`); 345 | 346 | const artifactsSnaphot = await this.repositories 347 | .doc((prPayload as any).repository.id.toString()) 348 | .collection('sizeArtifacts') 349 | .where('sha', '==', targetBranch.sha) 350 | .get(); 351 | 352 | if(artifactsSnaphot.empty) { 353 | return []; 354 | } 355 | 356 | return artifactsSnaphot.docs.map(doc => doc.data() as BuildArtifact); 357 | } 358 | 359 | /** 360 | * Retrieves the build artifacts from circleci 361 | */ 362 | async getCircleCIArtifacts(username: string, project: string, buildNumber: number, exclude?: string[], include?: string[]): Promise { 363 | const artifactUrl = `https://circleci.com/api/v2/project/gh/${username}/${project}/${buildNumber}/artifacts`; 364 | this.logDebug(`[size] Fetching artifacts for ${artifactUrl}`); 365 | 366 | const headers: HeadersInit = {}; 367 | const token = firebaseFunctionConfig().circleci.token; 368 | if (token !== undefined) { 369 | headers['Circle-Token'] = token; 370 | } 371 | 372 | const artifactsResponse = await fetch(artifactUrl, {headers, follow: 100}); 373 | 374 | let {items: artifacts} = (await artifactsResponse.json() as {items: CircleCiArtifact[]}); 375 | if (include) { 376 | artifacts = artifacts.filter(ca => include.some(path => ca.path.startsWith(path))); 377 | } 378 | if (exclude && exclude.length > 0) { 379 | artifacts = artifacts.filter(ca => !exclude.some(path => ca.path.startsWith(path))); 380 | } 381 | 382 | const buildArtifacts = []; 383 | 384 | for (const artifact of artifacts) { 385 | let response = await fetch(artifact.url); 386 | if (response.status >= 500) { 387 | response = await fetch(artifact.url); 388 | } 389 | 390 | if (!response.ok) { 391 | throw new Error(`fetch for ${artifact.url} returned status [${response.status}]: ${response.statusText}`); 392 | } 393 | 394 | const data = await response.arrayBuffer(); 395 | const size = data.byteLength; 396 | const pathParts = artifact.path.split('/'); 397 | 398 | this.logDebug(`[size] Fetched artifact '${artifact.path}' with size ${size}`); 399 | 400 | buildArtifacts.push({ 401 | path: artifact.path, 402 | url: artifact.url, 403 | size, 404 | projectName: pathParts.length > 1 ? pathParts[0] : undefined, 405 | context: pathParts.length > 2 ? pathParts.slice(1, -1).join('/') : undefined, 406 | filename: pathParts[pathParts.length - 1], 407 | }); 408 | } 409 | 410 | return buildArtifacts; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /functions/src/plugins/task.ts: -------------------------------------------------------------------------------- 1 | import {Application, Context} from "probot"; 2 | import Github from "@octokit/rest"; 3 | import {STATUS_STATE} from "../typings"; 4 | import {appConfig, AppConfig} from "../default"; 5 | 6 | export const CONFIG_FILE = "angular-robot.yml"; 7 | 8 | export class Task { 9 | repositories: FirebaseFirestore.CollectionReference; 10 | pullRequests: FirebaseFirestore.CollectionReference; 11 | admin: FirebaseFirestore.CollectionReference; 12 | config: FirebaseFirestore.CollectionReference; 13 | 14 | constructor(public robot: Application, public db: FirebaseFirestore.Firestore) { 15 | this.repositories = this.db.collection('repositories'); 16 | this.pullRequests = this.db.collection('pullRequests'); 17 | this.admin = this.db.collection('admin'); 18 | this.config = this.db.collection('config'); 19 | } 20 | 21 | /** 22 | * Gets the PR data from Github (or parameter) and adds/updates it in Firebase 23 | */ 24 | async updateDbPR(github: Github, owner: string, repo: string, number: number, repositoryId: number, newData?: any): Promise { 25 | newData = newData || (await github.pullRequests.get({owner, repo, number})).data; 26 | const data = {...newData, repository: {owner, name: repo, id: repositoryId}}; 27 | const doc = this.pullRequests.doc(data.id.toString()); 28 | await doc.set(data, {merge: true}).catch(err => { 29 | this.robot.log.error(err); 30 | throw err; 31 | }); 32 | return (await doc.get()).data(); 33 | } 34 | 35 | /** 36 | * Sets the status on the target PR 37 | */ 38 | async setStatus(state: STATUS_STATE, desc: string, statusContext: string, context: Context): Promise { 39 | const {owner, repo} = context.repo(); 40 | 41 | const statusParams: Github.ReposCreateStatusParams = { 42 | owner, 43 | repo, 44 | sha: context.payload.sha, 45 | context: statusContext, 46 | state, 47 | description: desc, 48 | }; 49 | 50 | await context.github.repos.createStatus(statusParams); 51 | } 52 | 53 | /** 54 | * Finds a PR that's previously been processed by the bot 55 | */ 56 | async findPrBySha(sha: string, repositoryId: number): Promise { 57 | const matches = await this.pullRequests 58 | .where('head.sha', '==', sha) 59 | .where('repository.id', '==', repositoryId) 60 | .get(); 61 | 62 | if(matches.empty) { 63 | return undefined; 64 | } 65 | 66 | return matches.docs[0].data() as Github.PullRequestsGetResponse; 67 | } 68 | 69 | // wrapper for this.robot.on 70 | dispatch(events: string | string[], callback: (context: Context) => any) { 71 | this.robot.on(events, (context: Context) => { 72 | this.log({context}, "Event received"); 73 | return callback(context); 74 | }); 75 | } 76 | 77 | log(...content: any[]) { 78 | this.robot.log.info(...content); 79 | } 80 | 81 | logInfo(...content: any[]) { 82 | this.log(...content); 83 | } 84 | 85 | logError(...content: any[]) { 86 | this.robot.log.error(...content); 87 | } 88 | 89 | logWarn(...content: any[]) { 90 | this.robot.log.warn(...content); 91 | } 92 | 93 | logDebug(...content: any[]) { 94 | this.robot.log.debug(...content); 95 | } 96 | 97 | /** 98 | * Returns the GraphQL node_id for a resource 99 | * @param resource the resource for which you want to get the node_id (eg: issue, or pull_request) 100 | * @returns {Promise} 101 | */ 102 | async node(context: Context, resource: any) { 103 | // GraphQL query to get Node id for any resource, which is needed for mutations 104 | const getResource = ` 105 | query getResource($url: URI!) { 106 | resource(url: $url) { 107 | ... on Node { 108 | id 109 | } 110 | } 111 | } 112 | `; 113 | 114 | return context.github.query(getResource, {url: resource.html_url}); 115 | } 116 | 117 | /** 118 | * Returns the app config for a repository 119 | */ 120 | async getAppConfig(context: Context): Promise { 121 | let repositoryConfig: AppConfig; 122 | const repositoryId = context.payload.repository.id; 123 | 124 | // Get the config from the database 125 | const doc = this.config.doc(repositoryId.toString()); 126 | const docData = (await doc.get()).data(); 127 | 128 | if(docData) { 129 | repositoryConfig = JSON.parse((docData).data) as AppConfig; 130 | } else { 131 | // If there is no config in the database, retrieve it from Github 132 | repositoryConfig = await this.refreshConfig(context); 133 | } 134 | 135 | return repositoryConfig; 136 | } 137 | 138 | /** 139 | * Retrieves the app config from Github, caches it in Firebase and returns it. 140 | */ 141 | async refreshConfig(context: Context): Promise { 142 | const repositoryConfig = await context.config(CONFIG_FILE, appConfig); 143 | const repositoryId = context.payload.repository.id; 144 | const doc = this.config.doc(repositoryId.toString()); 145 | // We need to stringify the config because Firebase throws on sub-keys with arrays 146 | await doc.set({data: JSON.stringify(repositoryConfig)}).catch(err => { 147 | this.robot.log.error(err); 148 | throw err; 149 | }); 150 | return repositoryConfig; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /functions/src/plugins/triage.ts: -------------------------------------------------------------------------------- 1 | import {Application, Context} from "probot"; 2 | import {Task} from "./task"; 3 | import {AdminConfig, TriageConfig} from "../default"; 4 | import {getLabelsNames, matchAllOfAny} from "./common"; 5 | import Github from '@octokit/rest'; 6 | 7 | export class TriageTask extends Task { 8 | constructor(robot: Application, db: FirebaseFirestore.Firestore) { 9 | super(robot, db); 10 | 11 | // TODO(ocombe): add a debounce for labeled events per issue 12 | this.dispatch([ 13 | 'issues.labeled', 14 | 'issues.unlabeled', 15 | 'issues.demilestoned', 16 | 'issues.milestoned', 17 | 'issues.opened' 18 | ], this.checkTriage.bind(this)); 19 | } 20 | 21 | async manualInit(): Promise { 22 | const adminConfig = await this.admin.doc('config').get(); 23 | if(adminConfig.exists && (adminConfig.data()).allowInit) { 24 | const github = await this.robot.auth(); 25 | const installations = await github.paginate(github.apps.listInstallations({}), pages => (pages as any as Github.AnyResponse).data); 26 | await Promise.all(installations.map(async installation => { 27 | const authGithub = await this.robot.auth(installation.id); 28 | const repositories = await authGithub.apps.listRepos({}); 29 | await Promise.all(repositories.data.repositories.map(async (repository: Github.AppsListReposResponseRepositoriesItem) => { 30 | const context = new Context({payload: {repository}}, authGithub, this.robot.log); 31 | const config = await this.getConfig(context); 32 | if(config.disabled) { 33 | return; 34 | } 35 | const {owner, repo} = context.repo(); 36 | const issues = await authGithub.paginate(authGithub.issues.listForRepo({ 37 | owner, 38 | repo, 39 | state: 'open', 40 | per_page: 100 41 | }), pages => (pages as any as Github.AnyResponse).data); 42 | 43 | issues.forEach(async (issue: Github.IssuesListForRepoResponseItem) => { 44 | // PRs are issues for github, but we don't want them here 45 | if(!issue.pull_request) { 46 | const isL1Triaged = this.isTriaged(config.l1TriageLabels, issue.labels.map((label: Github.IssuesListForRepoResponseItemLabelsItem) => label.name)); 47 | if(!isL1Triaged) { 48 | if(issue.milestone) { 49 | await this.setMilestone(null, context.github, owner, repo, issue); 50 | } 51 | } else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) { 52 | const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, issue.labels.map((label: Github.IssuesListForRepoResponseItemLabelsItem) => label.name)); 53 | if(isL2Triaged) { 54 | if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) { 55 | await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue); 56 | } 57 | } else { 58 | // if it's not triaged, set the "needsTriage" milestone 59 | if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) { 60 | await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue); 61 | } 62 | } 63 | } 64 | } 65 | }); 66 | })); 67 | })); 68 | } else { 69 | this.logError(`Manual init is disabled: the value of allowInit is set to false in the admin config database`); 70 | } 71 | } 72 | 73 | async checkTriage(context: Context): Promise { 74 | if(!context.payload.pull_request && !context.payload.issue.pull_request) { 75 | const issue: Github.IssuesGetResponse = context.payload.issue; 76 | const config = await this.getConfig(context); 77 | if(config.disabled) { 78 | return; 79 | } 80 | const {owner, repo} = context.repo(); 81 | // getting labels from Github because we might be adding multiple labels at once 82 | const isL1Triaged = this.isTriaged(config.l1TriageLabels, getLabelsNames(issue.labels)); 83 | if(!isL1Triaged) { 84 | if(issue.milestone) { 85 | await this.setMilestone(null, context.github, owner, repo, issue); 86 | } 87 | } else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) { 88 | const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, getLabelsNames(issue.labels)); 89 | if(isL2Triaged) { 90 | if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) { 91 | await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue); 92 | } 93 | } else { 94 | // if it's not triaged, set the "needsTriage" milestone 95 | if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) { 96 | await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | setMilestone(milestoneNumber: number | null, github: Github, owner: string, repo: string, issue: Github.IssuesListForRepoResponseItem): Promise> { 104 | if(milestoneNumber) { 105 | this.log(`Adding milestone ${milestoneNumber} to issue ${issue.html_url}`); 106 | } else { 107 | this.log(`Removing milestone from issue ${issue.html_url}`); 108 | } 109 | return github.issues.update({owner, repo, number: issue.number, milestone: milestoneNumber}).catch(err => { 110 | throw err; 111 | }); 112 | } 113 | 114 | isTriaged(triagedLabels: string[][], currentLabels: string[]): boolean { 115 | return matchAllOfAny(currentLabels, triagedLabels); 116 | } 117 | 118 | /** 119 | * Gets the config for the merge plugin from Github or uses default if necessary 120 | */ 121 | async getConfig(context: Context): Promise { 122 | const repositoryConfig = await this.getAppConfig(context); 123 | const config = repositoryConfig.triage; 124 | config.defaultMilestone = parseInt(config.defaultMilestone as string, 10); 125 | config.needsTriageMilestone = parseInt(config.needsTriageMilestone as string, 10); 126 | return config; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /functions/src/plugins/triagePR.ts: -------------------------------------------------------------------------------- 1 | import {Application, Context} from "probot"; 2 | import {Task} from "./task"; 3 | import {AdminConfig, TriageConfig} from "../default"; 4 | import {getLabelsNames, matchAllOfAny} from "./common"; 5 | import Github from '@octokit/rest'; 6 | 7 | export class TriagePRTask extends Task { 8 | constructor(robot: Application, db: FirebaseFirestore.Firestore) { 9 | super(robot, db); 10 | 11 | // PRs are issues for github 12 | this.dispatch([ 13 | 'pull_request.labeled', 14 | 'pull_request.unlabeled', 15 | 'issues.demilestoned', 16 | 'issues.milestoned', 17 | 'issues.opened' 18 | ], this.checkTriage.bind(this)); 19 | } 20 | 21 | async manualInit(): Promise { 22 | this.log('init triage PR'); 23 | const adminConfig = await this.admin.doc('config').get(); 24 | if(adminConfig.exists && (adminConfig.data()).allowInit) { 25 | const github = await this.robot.auth(); 26 | const installations = await github.paginate(github.apps.listInstallations({}), pages => (pages as any as Github.AnyResponse).data); 27 | await Promise.all(installations.map(async installation => { 28 | const authGithub = await this.robot.auth(installation.id); 29 | const repositories = await authGithub.apps.listRepos({}); 30 | await Promise.all(repositories.data.repositories.map(async (repository: Github.AppsListReposResponseRepositoriesItem) => { 31 | const context = new Context({payload: {repository}}, authGithub, this.robot.log); 32 | const config = await this.getConfig(context); 33 | if(config.disabled) { 34 | return; 35 | } 36 | const {owner, repo} = context.repo(); 37 | const issues = await authGithub.paginate(authGithub.issues.listForRepo({ 38 | owner, 39 | repo, 40 | state: 'open', 41 | per_page: 100 42 | }), pages => (pages as any as Github.AnyResponse).data); 43 | 44 | issues.forEach(async (issue: Github.IssuesListForRepoResponseItem) => { 45 | // We only want the PRs, not the issues 46 | if(issue.pull_request) { 47 | const isL1Triaged = this.isTriaged(config.l1TriageLabels, issue.labels.map((label: Github.IssuesListForRepoResponseItemLabelsItem) => label.name)); 48 | if(!isL1Triaged) { 49 | if(issue.milestone) { 50 | await this.setMilestone(null, context.github, owner, repo, issue); 51 | } 52 | } else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) { 53 | const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, issue.labels.map((label: Github.IssuesListForRepoResponseItemLabelsItem) => label.name)); 54 | if(isL2Triaged) { 55 | if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) { 56 | await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue); 57 | } 58 | } else { 59 | // if it's not triaged, set the "needsTriage" milestone 60 | if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) { 61 | await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue); 62 | } 63 | } 64 | } 65 | } 66 | }); 67 | })); 68 | })); 69 | } else { 70 | this.logError(`Manual init is disabled: the value of allowInit is set to false in the admin config database`); 71 | } 72 | } 73 | 74 | async checkTriage(context: Context): Promise { 75 | if((context.payload.issue && context.payload.issue.pull_request) || context.payload.pull_request) { 76 | const pr: Github.PullRequestsGetResponse | Github.IssuesGetResponse = context.payload.pull_request || context.payload.issue; 77 | const config = await this.getConfig(context); 78 | if(config.disabled) { 79 | return; 80 | } 81 | const {owner, repo} = context.repo(); 82 | const isL1Triaged = this.isTriaged(config.l1TriageLabels, getLabelsNames(pr.labels)); 83 | if(!isL1Triaged) { 84 | if(pr.milestone) { 85 | await this.setMilestone(null, context.github, owner, repo, pr); 86 | } 87 | } else if(!pr.milestone || pr.milestone.number === config.defaultMilestone || pr.milestone.number === config.needsTriageMilestone) { 88 | const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, getLabelsNames(pr.labels)); 89 | if(isL2Triaged) { 90 | if(!pr.milestone || pr.milestone.number !== config.defaultMilestone) { 91 | await this.setMilestone(config.defaultMilestone, context.github, owner, repo, pr); 92 | } 93 | } else { 94 | // if it's not triaged, set the "needsTriage" milestone 95 | if(!pr.milestone || pr.milestone.number !== config.needsTriageMilestone) { 96 | await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, pr); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | setMilestone(milestoneNumber: number | null, github: Github, owner: string, repo: string, PR: Github.PullRequestsGetResponse|Github.IssuesListForRepoResponseItem): Promise> { 104 | if(milestoneNumber) { 105 | this.log(`Adding milestone ${milestoneNumber} to PR ${PR.html_url}`); 106 | } else { 107 | this.log(`Removing milestone from PR ${PR.html_url}`); 108 | } 109 | return github.issues.update({owner, repo, number: PR.number, milestone: milestoneNumber}).catch(err => { 110 | throw err; 111 | }); 112 | } 113 | 114 | isTriaged(triagedLabels: string[][], currentLabels: string[]): boolean { 115 | return matchAllOfAny(currentLabels, triagedLabels); 116 | } 117 | 118 | /** 119 | * Gets the config for the merge plugin from Github or uses default if necessary 120 | */ 121 | async getConfig(context: Context): Promise { 122 | const repositoryConfig = await this.getAppConfig(context); 123 | const config = repositoryConfig.triagePR; 124 | config.defaultMilestone = parseInt(config.defaultMilestone as string, 10); 125 | config.needsTriageMilestone = parseInt(config.needsTriageMilestone as string, 10); 126 | return config; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /functions/src/typings.ts: -------------------------------------------------------------------------------- 1 | import Github from '@octokit/rest'; 2 | 3 | export const enum FILE_STATUS { 4 | Added = 'added', 5 | Modified = 'modified', 6 | Deleted = 'deleted' 7 | } 8 | 9 | export const enum STATUS_STATE { 10 | Pending = 'pending', 11 | Success = 'success', 12 | Failure = 'failure', 13 | Error = 'error' 14 | } 15 | 16 | export const enum GQL_STATUS_STATE { 17 | Pending = 'PENDING', 18 | Success = 'SUCCESS', 19 | Failure = 'FAILURE', 20 | Error = 'ERROR' 21 | } 22 | 23 | export const enum REVIEW_STATE { 24 | Pending = 'PENDING', 25 | Approved = 'APPROVED', 26 | ChangesRequest = 'CHANGES_REQUESTED', 27 | Commented = 'COMMENTED', 28 | Dismissed = 'DISMISSED' 29 | } 30 | 31 | export const enum AUTHOR_ASSOCIATION { 32 | // Author has been invited to collaborate on the repository. 33 | Collaborator = 'COLLABORATOR', 34 | // Author has previously committed to the repository. 35 | Contributor = 'CONTRIBUTOR', 36 | // Author has not previously committed to GitHub. 37 | FirstTimer = 'FIRST_TIMER', 38 | // Author has not previously committed to the repository. 39 | FirstTimeContributor = 'FIRST_TIME_CONTRIBUTOR', 40 | // Author is a member of the organization that owns the repository. 41 | Member = 'MEMBER', 42 | // Author has no association with the repository. 43 | None = 'NONE', 44 | // Author is the owner of the repository. 45 | Owner = 'OWNER' 46 | } 47 | 48 | export interface CachedPullRequest extends Github.PullRequestsGetResponse { 49 | pendingReviews?: number; 50 | } 51 | 52 | declare namespace GithubGQL { 53 | export interface PullRequest { 54 | labels: Labels; 55 | commits: Commits; 56 | } 57 | 58 | export interface Labels { 59 | nodes: Github.PullRequestsGetResponseLabelsItem[]; 60 | } 61 | 62 | export interface Commits { 63 | nodes: { 64 | id: string; 65 | commit: Commit; 66 | pullRequest: PullRequest; 67 | resourcePath: string; 68 | url: string; 69 | }[]; 70 | } 71 | 72 | export interface Commit { 73 | status: Status|null; 74 | } 75 | 76 | export interface Status { 77 | contexts: StatusContext[]; 78 | } 79 | 80 | interface StatusContext { 81 | // node id 82 | id: string; 83 | state: GQL_STATUS_STATE | STATUS_STATE; 84 | description: string; 85 | // name of the status, e.g. "ci/angular: merge status" 86 | context: string; 87 | // e.g. "2019-01-30T13:56:48Z" 88 | createdAt: string; 89 | } 90 | } 91 | 92 | export interface Commit { 93 | id: string; 94 | tree_id: string; 95 | distinct: boolean; 96 | message: string; 97 | timestamp: string; 98 | url: string; 99 | author: { 100 | name: string; 101 | email: string; 102 | username: string; 103 | }; 104 | committer: { 105 | name: string; 106 | email: string; 107 | username: string; 108 | }; 109 | added: string[]; 110 | removed: string[]; 111 | modified: string[]; 112 | } 113 | 114 | export default GithubGQL; 115 | -------------------------------------------------------------------------------- /functions/src/util.ts: -------------------------------------------------------------------------------- 1 | import {Application} from "probot"; 2 | import {CommonTask} from "./plugins/common"; 3 | import {MergeTask} from "./plugins/merge"; 4 | import {TriageTask} from "./plugins/triage"; 5 | import {SizeTask} from "./plugins/size"; 6 | import {TriagePRTask} from "./plugins/triagePR"; 7 | import {RerunCircleCITask} from "./plugins/rerun-circleci"; 8 | 9 | 10 | class Stream { 11 | constructor(private store: FirebaseFirestore.Firestore) { 12 | } 13 | 14 | write(data: any) { 15 | let log = console.log; 16 | let level = 'info'; 17 | if(data.level === 60) {// fatal 18 | log = console.error; 19 | level = 'fatal'; 20 | } else if(data.level === 50) {// error 21 | log = console.error; 22 | level = 'error'; 23 | } else if(data.level === 40) {// warn 24 | log = console.warn; 25 | level = 'warn'; 26 | } else if(data.level === 30) {// info 27 | level = 'info'; 28 | log = console.info; 29 | } else if(data.level === 20) {// debug 30 | level = 'debug'; 31 | log = console.log; 32 | } else if(data.level === 10) {// trace 33 | level = 'trace'; 34 | log = console.log; 35 | } 36 | 37 | let event = ''; 38 | let extraData = ''; 39 | const context = data.context; 40 | if(context) { 41 | event = context.name; 42 | const payload = data.context.payload; 43 | 44 | let path = ''; 45 | switch(event) { 46 | case 'pull_request': 47 | path = payload.pull_request.html_url; 48 | break; 49 | case 'pull_request_review': 50 | path = payload.review.html_url; 51 | break; 52 | case 'issues': 53 | path = payload.issue.html_url; 54 | break; 55 | case 'push': 56 | path = payload.compare; 57 | break; 58 | case 'status': 59 | path = payload.commit.html_url; 60 | break; 61 | case 'installation': 62 | path = payload.installation.html_url; 63 | break; 64 | case 'installation_repositories': 65 | path = payload.installation.html_url; 66 | break; 67 | } 68 | 69 | if(payload.action) { 70 | event += `.${payload.action}`; 71 | } 72 | event = `[${event}]`; 73 | 74 | extraData = ` [${context.id}|${path}]`; 75 | if(context.id) { 76 | this.store.collection('events').doc(context.id).set(context.payload).catch(err => { 77 | throw err; 78 | }); 79 | } 80 | } 81 | 82 | log(`[${level}]${event} ${typeof data === 'object' ? (data.err && data.err.stack ? data.err.stack : data.msg) : data}${extraData}`); 83 | } 84 | } 85 | 86 | /** 87 | * Stream Probot logs to console for Firebase 88 | */ 89 | export const consoleStream = (store: FirebaseFirestore.Firestore) => ({ 90 | type: "raw", 91 | level: "debug", 92 | stream: new Stream(store) 93 | }); 94 | 95 | export interface Tasks { 96 | commonTask: CommonTask; 97 | mergeTask: MergeTask; 98 | triageTask: TriageTask; 99 | triagePRTask: TriagePRTask; 100 | sizeTask: SizeTask; 101 | rerunCircleCiTask: RerunCircleCITask; 102 | } 103 | 104 | export function registerTasks(robot: Application, store: FirebaseFirestore.Firestore): Tasks { 105 | store.settings({timestampsInSnapshots: true}); 106 | return { 107 | commonTask: new CommonTask(robot, store), 108 | mergeTask: new MergeTask(robot, store), 109 | triageTask: new TriageTask(robot, store), 110 | triagePRTask: new TriagePRTask(robot, store), 111 | sizeTask: new SizeTask(robot, store), 112 | rerunCircleCiTask: new RerunCircleCITask(robot, store), 113 | }; 114 | } 115 | 116 | // copied from https://github.com/firebase/firebase-admin-node/blob/master/src/auth/credential.ts#L61 117 | function copyAttr(to: any, from: any, key: string, alt: string) { 118 | const tmp = from[key] || from[alt]; 119 | if(typeof tmp !== 'undefined') { 120 | to[key] = tmp; 121 | } 122 | } 123 | 124 | export function loadFirebaseConfig(params?: string | object) { 125 | let config; 126 | if(!params) { 127 | return; 128 | } else if(typeof params === 'string') { 129 | config = require(params); 130 | } else { 131 | config = params; 132 | } 133 | copyAttr(config, config, 'projectId', 'project_id'); 134 | copyAttr(config, config, 'privateKey', 'private_key'); 135 | copyAttr(config, config, 'clientId', 'client_id'); 136 | copyAttr(config, config, 'clientSecret', 'client_secret'); 137 | copyAttr(config, config, 'clientEmail', 'client_email'); 138 | copyAttr(config, config, 'refreshToken', 'refresh_token'); 139 | return config; 140 | } 141 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "es2017"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "noUnusedLocals": true, 12 | "noImplicitAny": true 13 | }, 14 | "compileOnSave": true, 15 | "include": [ 16 | "src" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-robot", 3 | "version": "0.1.0", 4 | "description": "A Github Bot to triage issues and PRs", 5 | "author": "Olivier Combe ", 6 | "license": "MIT", 7 | "repository": "https://github.com/angular/github-robot.git", 8 | "scripts": { 9 | "prebuild": "npm run lint && rimraf functions/lib", 10 | "build": "tsc -p functions", 11 | "build:dev": "tsc -p functions -w", 12 | "lint": "tslint -p tslint.json", 13 | "watch:functions": "tsc -p functions -w", 14 | "start:watch": "nodemon --watch ./functions/lib --watch ./functions/private --inspect functions/lib/dev.js ", 15 | "start:dev": "concurrently \"npm run build:dev\" \"npm run start:watch\"", 16 | "test": "jest --coverage", 17 | "test:dev": "jest --coverage --watch", 18 | "serve:functions": "firebase serve --only functions -p 3000", 19 | "start:functions": "firebase functions:shell", 20 | "deploy:functions:dev": "firebase deploy --only functions -P development", 21 | "deploy:functions:prod": "firebase deploy --only functions -P default", 22 | "logs:functions": "firebase functions:log" 23 | }, 24 | "engines": { 25 | "node": ">=14.0.0" 26 | }, 27 | "dependencies": { 28 | "@firebase/app-types": "0.3.2", 29 | "minimatch": "^3.0.4", 30 | "node-fetch": "2.2.0", 31 | "probot": "7.4.0" 32 | }, 33 | "devDependencies": { 34 | "@types/express": "4.16.0", 35 | "@types/github": "7.1.0", 36 | "@types/jasmine": "2.8.8", 37 | "@types/jest": "23.3.2", 38 | "@types/js-yaml": "3.11.2", 39 | "@types/minimatch": "^3.0.3", 40 | "@types/nock": "9.3.0", 41 | "@types/node": "16.18.1", 42 | "@types/node-fetch": "^2.1.2", 43 | "@types/request": "2.47.1", 44 | "concurrently": "4.0.1", 45 | "firebase-admin": "^11.0.0", 46 | "firebase-functions": "^4.0.2", 47 | "firebase-tools": "^11.0.1", 48 | "jasmine": "3.2.0", 49 | "jest": "23.6.0", 50 | "nock": "10.0.0", 51 | "nodemon": "1.18.4", 52 | "rimraf": "2.6.2", 53 | "smee-client": "1.0.2", 54 | "ts-jest": "23.10.0", 55 | "tslint": "^6.1.3", 56 | "typescript": "^4.8.4" 57 | }, 58 | "jest": { 59 | "moduleFileExtensions": [ 60 | "ts", 61 | "js", 62 | "json" 63 | ], 64 | "transform": { 65 | ".+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 66 | }, 67 | "testMatch": [ 68 | "**/test/*.(ts|js)" 69 | ], 70 | "coveragePathIgnorePatterns": [ 71 | "/node_modules/", 72 | "/test/", 73 | "/libs/" 74 | ], 75 | "collectCoverageFrom": [ 76 | "functions/src/**/*.{js,ts}", 77 | "!functions/src/dev.ts", 78 | "!functions/src/index.ts", 79 | "!functions/src/**/*.d.ts" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/common.spec.ts: -------------------------------------------------------------------------------- 1 | import {matchAny, matchAnyFile} from "../functions/src/plugins/common"; 2 | 3 | describe('common', () => { 4 | describe('match', () => { 5 | it('matchAnyFile should return true if it matches, false otherwise', async () => { 6 | const files = [ 7 | "packages/common/BUILD.bazel", 8 | "README.md" 9 | ]; 10 | const patterns = [ 11 | "BUILD.bazel", 12 | "LICENSE", 13 | "WORKSPACE", 14 | "modules/**", 15 | "packages/**", 16 | ]; 17 | const negPatterns = [ 18 | "packages/language-service/**", 19 | "**/.gitignore", 20 | "**/.gitkeep", 21 | ]; 22 | expect(matchAnyFile(files, [])).toBeFalsy(); 23 | expect(matchAnyFile(files, patterns)).toBeTruthy(); 24 | expect(matchAnyFile(files, patterns, negPatterns)).toBeTruthy(); 25 | expect(matchAnyFile(["BUILD.bazel"], patterns, negPatterns)).toBeTruthy(); 26 | expect(matchAnyFile(["packages/common/tsconfig.json"], patterns, negPatterns)).toBeTruthy(); 27 | expect(matchAnyFile(["packages/language-service/tsconfig.json"], patterns, negPatterns)).toBeFalsy(); 28 | 29 | const files2 = [ 30 | "packages/common/BUILD.bazel", 31 | ".gitignore" 32 | ]; 33 | expect(matchAnyFile(files2, patterns, negPatterns)).toBeTruthy(); 34 | expect(matchAnyFile([".gitkeep"], patterns, negPatterns)).toBeFalsy(); 35 | expect(matchAnyFile(["packages/common/.gitkeep"], patterns, negPatterns)).toBeFalsy(); 36 | }); 37 | 38 | it('matchAny should return true if it matches, false otherwise', async () => { 39 | expect(matchAny(["cla: no"], ["PR target: *", "cla: yes"])).toBeFalsy(); 40 | expect(matchAny(["type: bug/fix", "cla: yes"], ["PR target: *", "cla: yes"])).toBeTruthy(); 41 | expect(matchAny(["continuous-integration/travis-ci/pr"], ["continuous-integration/*", "code-review/pullapprove"])).toBeTruthy(); 42 | expect(matchAny(["PR target: master"], ["PR target: *", "cla: yes"], ["PR target: TBD", "cla: no"])).toBeTruthy(); 43 | expect(matchAny(["PR target: TBD"], ["PR target: *", "cla: yes"], ["PR target: TBD", "cla: no"])).toBeFalsy(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixtures/angular-robot.yml: -------------------------------------------------------------------------------- 1 | # Configuration for angular-robot 2 | 3 | #options for the size plugin 4 | size: 5 | # set to true to disable; or remove the size section completely 6 | disabled: false 7 | # byte value of maximum allowed change in size 8 | # can be an absolute number with optional units (kb, mb, gb) or a percentage 0-100% 9 | maxSizeIncrease: 1000 10 | # set to true (default: false) to enable PR comments with size change details 11 | # only comments if there is an actual change 12 | comment: false 13 | # set to control the set of artifacts 14 | # `include` filters to ensure only matching artifacts are analyzed 15 | # if `include` is not provided all artifacts are included 16 | # `exclude` filters to ensure matching artifacts are not analyzed 17 | # `exclude` takes priority over `include` 18 | # Both options can be a partial path or a full path of an artifact 19 | # if a partial path, all artifacts within that path will be matched 20 | # 21 | # The below configuration will include `/path/to/one/artifact.js` and all artifacts 22 | # within `/path/to/two/` except `/path/to/two/artifact2.js` 23 | # include: 24 | # - "/path/to/one/artifact.js" 25 | # - "/path/to/two/" 26 | # exclude: 27 | # - "/path/to/two/artifact2.js" 28 | status: 29 | # the name of the status shown in GitHub 30 | # the following is the default value 31 | context: "ci/angular: size" 32 | 33 | # options for the merge plugin 34 | merge: 35 | # the status will be added to your pull requests 36 | status: 37 | # set to true to disable 38 | disabled: false 39 | # the name of the status 40 | context: "ci/angular: merge status" 41 | # text to show when all checks pass 42 | successText: "All checks passed!" 43 | # text to show when some checks are failing 44 | failureText: "The following checks are failing:" 45 | 46 | # the g3 status will be added to your pull requests if they include files that match the patterns 47 | g3Status: 48 | # set to true to disable 49 | disabled: false 50 | # the name of the status 51 | context: "google3" 52 | # text to show when the status is pending, {{PRNumber}} will be replaced by the PR number 53 | pendingDesc: "Googler: run g3sync presubmit {{PRNumber}}" 54 | # text to show when the status is success 55 | successDesc: "Does not affect google3" 56 | # link to use for the details 57 | url: "http://go/angular-g3sync" 58 | # list of patterns to check for the files changed by the PR 59 | # this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky 60 | include: 61 | - "LICENSE" 62 | - "modules/**" 63 | - "packages/**" 64 | # list of patterns to ignore for the files changed by the PR 65 | exclude: 66 | - "packages/language-service/**" 67 | - "**/.gitignore" 68 | - "**/.gitkeep" 69 | - "**/tsconfig-build.json" 70 | - "**/tsconfig.json" 71 | - "**/rollup.config.js" 72 | - "**/BUILD.bazel" 73 | - "packages/**/test/**" 74 | 75 | # comment that will be added to a PR when there is a conflict, leave empty or set to false to disable 76 | mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges. 77 | \nPlease help to unblock it by resolving these conflicts. Thanks!" 78 | 79 | # label to monitor 80 | mergeLabel: "PR action: merge" 81 | 82 | # adding any of these labels will also add the merge label 83 | mergeLinkedLabels: 84 | - "PR action: merge-assistance" 85 | 86 | # list of checks that will determine if the merge label can be added 87 | checks: 88 | # whether the PR shouldn't have a conflict with the base branch 89 | noConflict: true 90 | # whether the PR should have all reviews completed. 91 | requireReviews: true 92 | # list of labels that a PR needs to have, checked with a regexp. 93 | requiredLabels: 94 | - "cla: yes" 95 | # list of labels that a PR needs to have, checked only AFTER the merge label has been applied 96 | requiredLabelsWhenMergeReady: 97 | - "PR target: *" 98 | # list of labels that a PR shouldn't have, checked after the required labels with a regexp 99 | forbiddenLabels: 100 | - "PR target: TBD" 101 | - "PR action: cleanup" 102 | - "PR action: review" 103 | - "PR state: blocked" 104 | - "cla: no" 105 | # list of PR statuses that need to be successful 106 | requiredStatuses: 107 | - "continuous-integration/travis-ci/pr" 108 | - "code-review/pullapprove" 109 | - "ci/circleci: build" 110 | - "ci/circleci: lint" 111 | 112 | # the comment that will be added when the merge label is added despite failing checks, leave empty or set to false to disable 113 | # {{MERGE_LABEL}} will be replaced by the value of the mergeLabel option 114 | # {{PLACEHOLDER}} will be replaced by the list of failing checks 115 | mergeRemovedComment: "I see that you just added the `{{MERGE_LABEL}}` label, but the following checks are still failing: 116 | \n{{PLACEHOLDER}} 117 | \n 118 | \n**If you want your PR to be merged, it has to pass all the CI checks.** 119 | \n 120 | \nIf you can't get the PR to a green state due to flakes or broken `main`, please try rebasing to `main` and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help." 121 | 122 | # options for the triage issues plugin 123 | triage: 124 | # set to true to disable 125 | disabled: false 126 | # number of the milestone to apply when the issue has not been triaged yet 127 | needsTriageMilestone: 83, 128 | # number of the milestone to apply when the issue is triaged 129 | defaultMilestone: 82, 130 | # arrays of labels that determine if an issue has been triaged by the caretaker 131 | l1TriageLabels: 132 | - 133 | - "comp: *" 134 | # arrays of labels that determine if an issue has been fully triaged 135 | l2TriageLabels: 136 | - 137 | - "type: bug/fix" 138 | - "severity*" 139 | - "freq*" 140 | - "comp: *" 141 | - 142 | - "type: feature" 143 | - "comp: *" 144 | - 145 | - "type: refactor" 146 | - "comp: *" 147 | - 148 | - "type: RFC / Discussion / question" 149 | - "comp: *" 150 | 151 | # options for the triage PR plugin 152 | triagePR: 153 | # set to true to disable 154 | disabled: false 155 | # number of the milestone to apply when the PR has not been triaged yet 156 | needsTriageMilestone: 83, 157 | # number of the milestone to apply when the PR is triaged 158 | defaultMilestone: 82, 159 | # arrays of labels that determine if a PR has been triaged by the caretaker 160 | l1TriageLabels: 161 | - 162 | - "comp: *" 163 | # arrays of labels that determine if a PR has been fully triaged 164 | l2TriageLabels: 165 | - 166 | - "type: *" 167 | - "effort*" 168 | - "risk*" 169 | - "comp: *" 170 | 171 | # options for the rerun circleCI plugin 172 | rerunCircleCI: 173 | # set to true to disable 174 | disabled: true 175 | # the label which when added triggers a rerun of the default CircleCI workflow. 176 | triggerRerunLabel: 'Trigger CircleCI Rerun' 177 | -------------------------------------------------------------------------------- /test/fixtures/github-api/get-files.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "sha": "dfbe08a9815657b74347f575635228dd333a4aff", 5 | "filename": "testFolder/testFile.md", 6 | "status": "added", 7 | "additions": 1, 8 | "deletions": 0, 9 | "changes": 1, 10 | "blob_url": "https://github.com/ocombe/test/blob/5d54547b7a7a24a0995e6b62bc2e342ffaa7131d/testFolder/testFile.md", 11 | "raw_url": "https://github.com/ocombe/test/raw/5d54547b7a7a24a0995e6b62bc2e342ffaa7131d/testFolder/testFile.md", 12 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/testFolder/testFile.md?ref=5d54547b7a7a24a0995e6b62bc2e342ffaa7131d", 13 | "patch": "@@ -0,0 +1 @@\n+hello <3" 14 | } 15 | ], 16 | "meta": { 17 | "x-ratelimit-limit": "5000", 18 | "x-ratelimit-remaining": "4976", 19 | "x-ratelimit-reset": "1517591637", 20 | "x-github-request-id": "FE3F:612C:3C09A41:8B99E10:5A748EC1", 21 | "x-github-media-type": "github.v3; format=json", 22 | "last-modified": "Fri, 02 Feb 2018 16:15:58 GMT", 23 | "etag": "32846808ed5791ef1276db63912c4c9e", 24 | "status": "200 OK" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/github-api/get-installation-repositories.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 108976240, 3 | "name": "test", 4 | "full_name": "ocombe/test", 5 | "owner": { 6 | "login": "ocombe", 7 | "id": 265378, 8 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/ocombe", 11 | "html_url": "https://github.com/ocombe", 12 | "followers_url": "https://api.github.com/users/ocombe/followers", 13 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 17 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 18 | "repos_url": "https://api.github.com/users/ocombe/repos", 19 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 21 | "type": "User", 22 | "site_admin": false 23 | }, 24 | "private": false, 25 | "html_url": "https://github.com/ocombe/test", 26 | "description": "Repository used to test things", 27 | "fork": false, 28 | "url": "https://api.github.com/repos/ocombe/test", 29 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 30 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 31 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 32 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 33 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 34 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 35 | "events_url": "https://api.github.com/repos/ocombe/test/events", 36 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 37 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 38 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 39 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 40 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 41 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 42 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 43 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 44 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 45 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 46 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 47 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 48 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 49 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 50 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 51 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 52 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 53 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 54 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 55 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 56 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 57 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 58 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 59 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 60 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 61 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 62 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 63 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 64 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 65 | "created_at": "2017-10-31T09:52:22Z", 66 | "updated_at": "2018-01-17T13:55:58Z", 67 | "pushed_at": "2018-01-23T10:35:22Z", 68 | "git_url": "git://github.com/ocombe/test.git", 69 | "ssh_url": "git@github.com:ocombe/test.git", 70 | "clone_url": "https://github.com/ocombe/test.git", 71 | "svn_url": "https://github.com/ocombe/test", 72 | "homepage": null, 73 | "size": 24, 74 | "stargazers_count": 0, 75 | "watchers_count": 0, 76 | "language": null, 77 | "has_issues": true, 78 | "has_projects": true, 79 | "has_downloads": true, 80 | "has_wiki": true, 81 | "has_pages": false, 82 | "forks_count": 0, 83 | "mirror_url": null, 84 | "archived": false, 85 | "open_issues_count": 14, 86 | "license": { 87 | "key": "mit", 88 | "name": "MIT License", 89 | "spdx_id": "MIT", 90 | "url": "https://api.github.com/licenses/mit" 91 | }, 92 | "forks": 0, 93 | "open_issues": 14, 94 | "watchers": 0, 95 | "default_branch": "master", 96 | "permissions": { 97 | "admin": false, 98 | "push": false, 99 | "pull": false 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/fixtures/github-api/get-milestone.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "url": "https://api.github.com/repos/ocombe/test/milestones/1", 4 | "html_url": "https://github.com/ocombe/test/milestone/1", 5 | "labels_url": "https://api.github.com/repos/ocombe/test/milestones/1/labels", 6 | "id": 3048627, 7 | "number": 1, 8 | "title": "Backlog", 9 | "description": "", 10 | "creator": { 11 | "login": "ocombe", 12 | "id": 265378, 13 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 14 | "gravatar_id": "", 15 | "url": "https://api.github.com/users/ocombe", 16 | "html_url": "https://github.com/ocombe", 17 | "followers_url": "https://api.github.com/users/ocombe/followers", 18 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 19 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 20 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 21 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 22 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 23 | "repos_url": "https://api.github.com/users/ocombe/repos", 24 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 25 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 26 | "type": "User", 27 | "site_admin": false 28 | }, 29 | "open_issues": 0, 30 | "closed_issues": 0, 31 | "state": "open", 32 | "created_at": "2018-01-19T14:19:55Z", 33 | "updated_at": "2018-01-19T14:19:55Z", 34 | "due_on": null, 35 | "closed_at": null 36 | }, 37 | "meta": { 38 | "x-ratelimit-limit": "5000", 39 | "x-ratelimit-remaining": "4986", 40 | "x-ratelimit-reset": "1516375348", 41 | "x-github-request-id": "E791:13FF:388C278:65C1AAF:5A6200F8", 42 | "x-github-media-type": "github.v3; format=json", 43 | "last-modified": "Fri, 19 Jan 2018 14:19:55 GMT", 44 | "etag": "\"9062447ecc19f8aa6e2d65f808c9988a\"", 45 | "status": "200 OK" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/fixtures/github-api/get-milestones.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "url": "https://api.github.com/repos/ocombe/test/milestones/1", 5 | "html_url": "https://github.com/ocombe/test/milestone/1", 6 | "labels_url": "https://api.github.com/repos/ocombe/test/milestones/1/labels", 7 | "id": 3048627, 8 | "number": 1, 9 | "title": "Backlog", 10 | "description": "", 11 | "creator": [ 12 | { 13 | "login": "ocombe", 14 | "id": 265378, 15 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 16 | "gravatar_id": "", 17 | "url": "https://api.github.com/users/ocombe", 18 | "html_url": "https://github.com/ocombe", 19 | "followers_url": "https://api.github.com/users/ocombe/followers", 20 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 21 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 22 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 23 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 24 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 25 | "repos_url": "https://api.github.com/users/ocombe/repos", 26 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 27 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 28 | "type": "User", 29 | "site_admin": false 30 | } 31 | ], 32 | "open_issues": 0, 33 | "closed_issues": 0, 34 | "state": "open", 35 | "created_at": "2018-01-19T14:19:55Z", 36 | "updated_at": "2018-01-19T14:19:55Z", 37 | "due_on": null, 38 | "closed_at": null 39 | } 40 | ], 41 | "meta": { 42 | "x-ratelimit-limit": "5000", 43 | "x-ratelimit-remaining": "4994", 44 | "x-ratelimit-reset": "1516375348", 45 | "x-github-request-id": "E642:13FE:2513BEA:4A61D2B:5A620010", 46 | "x-github-media-type": "github.v3; format=json", 47 | "etag": "\"8eb249078457060c7e2f33cc610d34f4\"", 48 | "status": "200 OK" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/fixtures/installation.created.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "installation", 3 | "name": "installation", 4 | "payload": { 5 | "action": "created", 6 | "installation": { 7 | "id": 78777, 8 | "account": { 9 | "login": "ocombe", 10 | "id": 265378, 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/ocombe", 14 | "html_url": "https://github.com/ocombe", 15 | "followers_url": "https://api.github.com/users/ocombe/followers", 16 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 20 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 21 | "repos_url": "https://api.github.com/users/ocombe/repos", 22 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "repository_selection": "selected", 28 | "access_tokens_url": "https://api.github.com/installations/78777/access_tokens", 29 | "repositories_url": "https://api.github.com/installation/repositories", 30 | "html_url": "https://github.com/settings/installations/78777", 31 | "app_id": 6359, 32 | "target_id": 265378, 33 | "target_type": "User", 34 | "permissions": { 35 | "pull_requests": "write", 36 | "issues": "write", 37 | "statuses": "write", 38 | "contents": "read", 39 | "metadata": "read" 40 | }, 41 | "events": [ 42 | "issues", 43 | "issue_comment", 44 | "pull_request", 45 | "pull_request_review", 46 | "pull_request_review_comment", 47 | "push", 48 | "status" 49 | ], 50 | "created_at": 1515417956, 51 | "updated_at": 1515417956, 52 | "single_file_name": null 53 | }, 54 | "repositories": [ 55 | { 56 | "id": 108976240, 57 | "name": "test", 58 | "full_name": "ocombe/test" 59 | } 60 | ], 61 | "sender": { 62 | "login": "ocombe", 63 | "id": 265378, 64 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 65 | "gravatar_id": "", 66 | "url": "https://api.github.com/users/ocombe", 67 | "html_url": "https://github.com/ocombe", 68 | "followers_url": "https://api.github.com/users/ocombe/followers", 69 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 70 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 71 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 72 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 73 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 74 | "repos_url": "https://api.github.com/users/ocombe/repos", 75 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 76 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 77 | "type": "User", 78 | "site_admin": false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/fixtures/installation_repositories.added.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "installation_repositories", 3 | "name": "installation_repositories", 4 | "payload": { 5 | "action": "added", 6 | "installation": { 7 | "id": 78777, 8 | "account": { 9 | "login": "ocombe", 10 | "id": 265378, 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/ocombe", 14 | "html_url": "https://github.com/ocombe", 15 | "followers_url": "https://api.github.com/users/ocombe/followers", 16 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 20 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 21 | "repos_url": "https://api.github.com/users/ocombe/repos", 22 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "repository_selection": "selected", 28 | "access_tokens_url": "https://api.github.com/installations/78777/access_tokens", 29 | "repositories_url": "https://api.github.com/installation/repositories", 30 | "html_url": "https://github.com/settings/installations/78777", 31 | "app_id": 6359, 32 | "target_id": 265378, 33 | "target_type": "User", 34 | "permissions": { 35 | "statuses": "write", 36 | "issues": "write", 37 | "pull_requests": "write", 38 | "metadata": "read", 39 | "contents": "read" 40 | }, 41 | "events": [ 42 | "issues", 43 | "issue_comment", 44 | "pull_request", 45 | "pull_request_review", 46 | "pull_request_review_comment", 47 | "push", 48 | "status" 49 | ], 50 | "created_at": 1515417956, 51 | "updated_at": 1515417957, 52 | "single_file_name": null 53 | }, 54 | "repository_selection": "selected", 55 | "repositories_added": [ 56 | { 57 | "id": 108976240, 58 | "name": "test", 59 | "full_name": "ocombe/test" 60 | } 61 | ], 62 | "repositories_removed": [ 63 | ], 64 | "sender": { 65 | "login": "ocombe", 66 | "id": 265378, 67 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 68 | "gravatar_id": "", 69 | "url": "https://api.github.com/users/ocombe", 70 | "html_url": "https://github.com/ocombe", 71 | "followers_url": "https://api.github.com/users/ocombe/followers", 72 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 73 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 74 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 75 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 76 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 77 | "repos_url": "https://api.github.com/users/ocombe/repos", 78 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 79 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 80 | "type": "User", 81 | "site_admin": false 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/fixtures/issues.labeled.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "issues", 3 | "name": "issues", 4 | "payload": { 5 | "action": "labeled", 6 | "issue": { 7 | "url": "https://api.github.com/repos/ocombe/test/issues/9", 8 | "repository_url": "https://api.github.com/repos/ocombe/test", 9 | "labels_url": "https://api.github.com/repos/ocombe/test/issues/9/labels{/name}", 10 | "comments_url": "https://api.github.com/repos/ocombe/test/issues/9/comments", 11 | "events_url": "https://api.github.com/repos/ocombe/test/issues/9/events", 12 | "html_url": "https://github.com/ocombe/test/issues/9", 13 | "id": 277339102, 14 | "number": 9, 15 | "title": "erere", 16 | "user": { 17 | "login": "ocombe", 18 | "id": 265378, 19 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 20 | "gravatar_id": "", 21 | "url": "https://api.github.com/users/ocombe", 22 | "html_url": "https://github.com/ocombe", 23 | "followers_url": "https://api.github.com/users/ocombe/followers", 24 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 25 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 26 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 27 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 28 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 29 | "repos_url": "https://api.github.com/users/ocombe/repos", 30 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 31 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 32 | "type": "User", 33 | "site_admin": false 34 | }, 35 | "labels": [ 36 | { 37 | "id": 769242745, 38 | "url": "https://api.github.com/repos/ocombe/test/labels/cla:%20no", 39 | "name": "cla: no", 40 | "color": "d93f0b", 41 | "default": false 42 | } 43 | ], 44 | "state": "open", 45 | "locked": false, 46 | "assignee": null, 47 | "assignees": [ 48 | ], 49 | "milestone": null, 50 | "comments": 0, 51 | "created_at": "2017-11-28T10:50:00Z", 52 | "updated_at": "2017-11-28T11:08:43Z", 53 | "closed_at": null, 54 | "author_association": "OWNER", 55 | "body": "eazdqsdfdfsfsdf" 56 | }, 57 | "label": { 58 | "id": 769242745, 59 | "url": "https://api.github.com/repos/ocombe/test/labels/cla:%20no", 60 | "name": "cla: no", 61 | "color": "d93f0b", 62 | "default": false 63 | }, 64 | "repository": { 65 | "id": 108976240, 66 | "name": "test", 67 | "full_name": "ocombe/test", 68 | "owner": { 69 | "login": "ocombe", 70 | "id": 265378, 71 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 72 | "gravatar_id": "", 73 | "url": "https://api.github.com/users/ocombe", 74 | "html_url": "https://github.com/ocombe", 75 | "followers_url": "https://api.github.com/users/ocombe/followers", 76 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 77 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 78 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 79 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 80 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 81 | "repos_url": "https://api.github.com/users/ocombe/repos", 82 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 83 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 84 | "type": "User", 85 | "site_admin": false 86 | }, 87 | "private": false, 88 | "html_url": "https://github.com/ocombe/test", 89 | "description": "Repository used to test things", 90 | "fork": false, 91 | "url": "https://api.github.com/repos/ocombe/test", 92 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 93 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 94 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 95 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 96 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 97 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 98 | "events_url": "https://api.github.com/repos/ocombe/test/events", 99 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 100 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 101 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 102 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 103 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 104 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 105 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 106 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 107 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 108 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 109 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 110 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 111 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 112 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 113 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 114 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 115 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 116 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 117 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 118 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 119 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 120 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 121 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 122 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 123 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 124 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 125 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 126 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 127 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 128 | "created_at": "2017-10-31T09:52:22Z", 129 | "updated_at": "2018-01-17T13:55:58Z", 130 | "pushed_at": "2018-01-18T16:52:43Z", 131 | "git_url": "git://github.com/ocombe/test.git", 132 | "ssh_url": "git@github.com:ocombe/test.git", 133 | "clone_url": "https://github.com/ocombe/test.git", 134 | "svn_url": "https://github.com/ocombe/test", 135 | "homepage": null, 136 | "size": 20, 137 | "stargazers_count": 0, 138 | "watchers_count": 0, 139 | "language": null, 140 | "has_issues": true, 141 | "has_projects": true, 142 | "has_downloads": true, 143 | "has_wiki": true, 144 | "has_pages": false, 145 | "forks_count": 0, 146 | "mirror_url": null, 147 | "archived": false, 148 | "open_issues_count": 14, 149 | "license": { 150 | "key": "mit", 151 | "name": "MIT License", 152 | "spdx_id": "MIT", 153 | "url": "https://api.github.com/licenses/mit" 154 | }, 155 | "forks": 0, 156 | "open_issues": 14, 157 | "watchers": 0, 158 | "default_branch": "master" 159 | }, 160 | "sender": { 161 | "login": "ocombe", 162 | "id": 265378, 163 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 164 | "gravatar_id": "", 165 | "url": "https://api.github.com/users/ocombe", 166 | "html_url": "https://github.com/ocombe", 167 | "followers_url": "https://api.github.com/users/ocombe/followers", 168 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 169 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 170 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 171 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 172 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 173 | "repos_url": "https://api.github.com/users/ocombe/repos", 174 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 175 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 176 | "type": "User", 177 | "site_admin": false 178 | }, 179 | "installation": { 180 | "id": 78813 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/fixtures/issues.milestoned.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "issues", 3 | "name": "issues", 4 | "payload": { 5 | "action": "milestoned", 6 | "issue": { 7 | "url": "https://api.github.com/repos/ocombe/test/issues/2", 8 | "repository_url": "https://api.github.com/repos/ocombe/test", 9 | "labels_url": "https://api.github.com/repos/ocombe/test/issues/2/labels{/name}", 10 | "comments_url": "https://api.github.com/repos/ocombe/test/issues/2/comments", 11 | "events_url": "https://api.github.com/repos/ocombe/test/issues/2/events", 12 | "html_url": "https://github.com/ocombe/test/issues/2", 13 | "id": 269985810, 14 | "number": 2, 15 | "title": "zeze", 16 | "user": { 17 | "login": "ocombe", 18 | "id": 265378, 19 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 20 | "gravatar_id": "", 21 | "url": "https://api.github.com/users/ocombe", 22 | "html_url": "https://github.com/ocombe", 23 | "followers_url": "https://api.github.com/users/ocombe/followers", 24 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 25 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 26 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 27 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 28 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 29 | "repos_url": "https://api.github.com/users/ocombe/repos", 30 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 31 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 32 | "type": "User", 33 | "site_admin": false 34 | }, 35 | "labels": [ 36 | { 37 | "id": 824969769, 38 | "url": "https://api.github.com/repos/ocombe/test/labels/comp:%20common", 39 | "name": "comp: common", 40 | "color": "f48f81", 41 | "default": false 42 | }, 43 | { 44 | "id": 810132959, 45 | "url": "https://api.github.com/repos/ocombe/test/labels/type:%20feature", 46 | "name": "type: feature", 47 | "color": "cfa7e8", 48 | "default": false 49 | } 50 | ], 51 | "state": "open", 52 | "locked": false, 53 | "assignee": null, 54 | "assignees": [ 55 | 56 | ], 57 | "milestone": { 58 | "url": "https://api.github.com/repos/ocombe/test/milestones/1", 59 | "html_url": "https://github.com/ocombe/test/milestone/1", 60 | "labels_url": "https://api.github.com/repos/ocombe/test/milestones/1/labels", 61 | "id": 3048627, 62 | "number": 1, 63 | "title": "Backlog", 64 | "description": "", 65 | "creator": { 66 | "login": "ocombe", 67 | "id": 265378, 68 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 69 | "gravatar_id": "", 70 | "url": "https://api.github.com/users/ocombe", 71 | "html_url": "https://github.com/ocombe", 72 | "followers_url": "https://api.github.com/users/ocombe/followers", 73 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 74 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 75 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 76 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 77 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 78 | "repos_url": "https://api.github.com/users/ocombe/repos", 79 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 80 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 81 | "type": "User", 82 | "site_admin": false 83 | }, 84 | "open_issues": 3, 85 | "closed_issues": 0, 86 | "state": "open", 87 | "created_at": "2018-01-19T14:19:55Z", 88 | "updated_at": "2018-02-09T00:57:49Z", 89 | "due_on": null, 90 | "closed_at": null 91 | }, 92 | "comments": 0, 93 | "created_at": "2017-10-31T14:45:12Z", 94 | "updated_at": "2018-02-09T00:57:49Z", 95 | "closed_at": null, 96 | "author_association": "OWNER", 97 | "body": "qssqssdqsqsqssd" 98 | }, 99 | "repository": { 100 | "id": 108976240, 101 | "name": "test", 102 | "full_name": "ocombe/test", 103 | "owner": { 104 | "login": "ocombe", 105 | "id": 265378, 106 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 107 | "gravatar_id": "", 108 | "url": "https://api.github.com/users/ocombe", 109 | "html_url": "https://github.com/ocombe", 110 | "followers_url": "https://api.github.com/users/ocombe/followers", 111 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 112 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 113 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 114 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 115 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 116 | "repos_url": "https://api.github.com/users/ocombe/repos", 117 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 118 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 119 | "type": "User", 120 | "site_admin": false 121 | }, 122 | "private": false, 123 | "html_url": "https://github.com/ocombe/test", 124 | "description": "Repository used to test things", 125 | "fork": false, 126 | "url": "https://api.github.com/repos/ocombe/test", 127 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 128 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 129 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 130 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 131 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 132 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 133 | "events_url": "https://api.github.com/repos/ocombe/test/events", 134 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 135 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 136 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 137 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 138 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 139 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 140 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 141 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 142 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 143 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 144 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 145 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 146 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 147 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 148 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 149 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 150 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 151 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 152 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 153 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 154 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 155 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 156 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 157 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 158 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 159 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 160 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 161 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 162 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 163 | "created_at": "2017-10-31T09:52:22Z", 164 | "updated_at": "2018-01-17T13:55:58Z", 165 | "pushed_at": "2018-02-08T22:24:40Z", 166 | "git_url": "git://github.com/ocombe/test.git", 167 | "ssh_url": "git@github.com:ocombe/test.git", 168 | "clone_url": "https://github.com/ocombe/test.git", 169 | "svn_url": "https://github.com/ocombe/test", 170 | "homepage": null, 171 | "size": 28, 172 | "stargazers_count": 0, 173 | "watchers_count": 0, 174 | "language": null, 175 | "has_issues": true, 176 | "has_projects": true, 177 | "has_downloads": true, 178 | "has_wiki": true, 179 | "has_pages": false, 180 | "forks_count": 0, 181 | "mirror_url": null, 182 | "archived": false, 183 | "open_issues_count": 16, 184 | "license": { 185 | "key": "mit", 186 | "name": "MIT License", 187 | "spdx_id": "MIT", 188 | "url": "https://api.github.com/licenses/mit" 189 | }, 190 | "forks": 0, 191 | "open_issues": 16, 192 | "watchers": 0, 193 | "default_branch": "master" 194 | }, 195 | "sender": { 196 | "login": "wheatley-dev[bot]", 197 | "id": 35229909, 198 | "avatar_url": "https://avatars2.githubusercontent.com/in/7968?v=4", 199 | "gravatar_id": "", 200 | "url": "https://api.github.com/users/wheatley-dev%5Bbot%5D", 201 | "html_url": "https://github.com/apps/wheatley-dev", 202 | "followers_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/followers", 203 | "following_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/following{/other_user}", 204 | "gists_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/gists{/gist_id}", 205 | "starred_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/starred{/owner}{/repo}", 206 | "subscriptions_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/subscriptions", 207 | "organizations_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/orgs", 208 | "repos_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/repos", 209 | "events_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/events{/privacy}", 210 | "received_events_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/received_events", 211 | "type": "Bot", 212 | "site_admin": false 213 | }, 214 | "installation": { 215 | "id": 78813 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /test/fixtures/issues.opened.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "issues", 3 | "name": "issues", 4 | "payload": { 5 | "action": "opened", 6 | "issue": { 7 | "url": "https://api.github.com/repos/ocombe/test/issues/7", 8 | "repository_url": "https://api.github.com/repos/ocombe/test", 9 | "labels_url": "https://api.github.com/repos/ocombe/test/issues/7/labels{/name}", 10 | "comments_url": "https://api.github.com/repos/ocombe/test/issues/7/comments", 11 | "events_url": "https://api.github.com/repos/ocombe/test/issues/7/events", 12 | "html_url": "https://github.com/ocombe/test/issues/7", 13 | "id": 270678640, 14 | "number": 7, 15 | "title": "sQFDF", 16 | "user": { 17 | "login": "ocombe", 18 | "id": 265378, 19 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 20 | "gravatar_id": "", 21 | "url": "https://api.github.com/users/ocombe", 22 | "html_url": "https://github.com/ocombe", 23 | "followers_url": "https://api.github.com/users/ocombe/followers", 24 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 25 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 26 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 27 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 28 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 29 | "repos_url": "https://api.github.com/users/ocombe/repos", 30 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 31 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 32 | "type": "User", 33 | "site_admin": false 34 | }, 35 | "labels": [ 36 | ], 37 | "state": "open", 38 | "locked": false, 39 | "assignee": null, 40 | "assignees": [ 41 | ], 42 | "milestone": null, 43 | "comments": 0, 44 | "created_at": "2017-11-02T14:42:20Z", 45 | "updated_at": "2017-11-02T14:42:20Z", 46 | "closed_at": null, 47 | "author_association": "OWNER", 48 | "body": "" 49 | }, 50 | "repository": { 51 | "id": 108976240, 52 | "name": "test", 53 | "full_name": "ocombe/test", 54 | "owner": { 55 | "login": "ocombe", 56 | "id": 265378, 57 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 58 | "gravatar_id": "", 59 | "url": "https://api.github.com/users/ocombe", 60 | "html_url": "https://github.com/ocombe", 61 | "followers_url": "https://api.github.com/users/ocombe/followers", 62 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 63 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 64 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 65 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 66 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 67 | "repos_url": "https://api.github.com/users/ocombe/repos", 68 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 69 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 70 | "type": "User", 71 | "site_admin": false 72 | }, 73 | "private": false, 74 | "html_url": "https://github.com/ocombe/test", 75 | "description": "Repository used to test things", 76 | "fork": false, 77 | "url": "https://api.github.com/repos/ocombe/test", 78 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 79 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 80 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 81 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 82 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 83 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 84 | "events_url": "https://api.github.com/repos/ocombe/test/events", 85 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 86 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 87 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 88 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 89 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 90 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 91 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 92 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 93 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 94 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 95 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 96 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 97 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 98 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 99 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 100 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 101 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 102 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 103 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 104 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 105 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 106 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 107 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 108 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 109 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 110 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 111 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 112 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 113 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 114 | "created_at": "2017-10-31T09:52:22Z", 115 | "updated_at": "2017-10-31T09:52:22Z", 116 | "pushed_at": "2017-10-31T14:42:18Z", 117 | "git_url": "git://github.com/ocombe/test.git", 118 | "ssh_url": "git@github.com:ocombe/test.git", 119 | "clone_url": "https://github.com/ocombe/test.git", 120 | "svn_url": "https://github.com/ocombe/test", 121 | "homepage": null, 122 | "size": 2, 123 | "stargazers_count": 0, 124 | "watchers_count": 0, 125 | "language": null, 126 | "has_issues": true, 127 | "has_projects": true, 128 | "has_downloads": true, 129 | "has_wiki": true, 130 | "has_pages": false, 131 | "forks_count": 0, 132 | "mirror_url": null, 133 | "archived": false, 134 | "open_issues_count": 6, 135 | "forks": 0, 136 | "open_issues": 6, 137 | "watchers": 0, 138 | "default_branch": "master" 139 | }, 140 | "sender": { 141 | "login": "ocombe", 142 | "id": 265378, 143 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 144 | "gravatar_id": "", 145 | "url": "https://api.github.com/users/ocombe", 146 | "html_url": "https://github.com/ocombe", 147 | "followers_url": "https://api.github.com/users/ocombe/followers", 148 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 149 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 150 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 151 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 152 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 153 | "repos_url": "https://api.github.com/users/ocombe/repos", 154 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 155 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 156 | "type": "User", 157 | "site_admin": false 158 | }, 159 | "installation": { 160 | "id": 63922 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /test/fixtures/push.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "push", 3 | "name": "push", 4 | "payload": { 5 | "ref": "refs/heads/master", 6 | "before": "f1ea7b6d00830786172bd0282e5c9f3d4424e79f", 7 | "after": "000429b21c9922ba743c9ada988b0afa76c83a98", 8 | "created": false, 9 | "deleted": false, 10 | "forced": false, 11 | "base_ref": null, 12 | "compare": "https://github.com/ocombe/test/compare/f1ea7b6d0083...000429b21c99", 13 | "commits": [ 14 | { 15 | "id": "000429b21c9922ba743c9ada988b0afa76c83a98", 16 | "tree_id": "7d70a9df66d66cabe2d5917a5895e6cc7e10a1fb", 17 | "distinct": true, 18 | "message": "Update README.md", 19 | "timestamp": "2017-11-29T16:44:43+01:00", 20 | "url": "https://github.com/ocombe/test/commit/000429b21c9922ba743c9ada988b0afa76c83a98", 21 | "author": { 22 | "name": "Olivier Combe", 23 | "email": "olivier.combe@gmail.com", 24 | "username": "ocombe" 25 | }, 26 | "committer": { 27 | "name": "GitHub", 28 | "email": "noreply@github.com", 29 | "username": "web-flow" 30 | }, 31 | "added": [ 32 | ], 33 | "removed": [ 34 | ], 35 | "modified": [ 36 | ] 37 | } 38 | ], 39 | "head_commit": { 40 | "id": "000429b21c9922ba743c9ada988b0afa76c83a98", 41 | "tree_id": "7d70a9df66d66cabe2d5917a5895e6cc7e10a1fb", 42 | "distinct": true, 43 | "message": "Update README.md", 44 | "timestamp": "2017-11-29T16:44:43+01:00", 45 | "url": "https://github.com/ocombe/test/commit/000429b21c9922ba743c9ada988b0afa76c83a98", 46 | "author": { 47 | "name": "Olivier Combe", 48 | "email": "olivier.combe@gmail.com", 49 | "username": "ocombe" 50 | }, 51 | "committer": { 52 | "name": "GitHub", 53 | "email": "noreply@github.com", 54 | "username": "web-flow" 55 | }, 56 | "added": [ 57 | ], 58 | "removed": [ 59 | ], 60 | "modified": [ 61 | ] 62 | }, 63 | "repository": { 64 | "id": 108976240, 65 | "name": "test", 66 | "full_name": "ocombe/test", 67 | "owner": { 68 | "name": "ocombe", 69 | "email": "olivier.combe@gmail.com", 70 | "login": "ocombe", 71 | "id": 265378, 72 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 73 | "gravatar_id": "", 74 | "url": "https://api.github.com/users/ocombe", 75 | "html_url": "https://github.com/ocombe", 76 | "followers_url": "https://api.github.com/users/ocombe/followers", 77 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 78 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 79 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 80 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 81 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 82 | "repos_url": "https://api.github.com/users/ocombe/repos", 83 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 84 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 85 | "type": "User", 86 | "site_admin": false 87 | }, 88 | "private": false, 89 | "html_url": "https://github.com/ocombe/test", 90 | "description": "Repository used to test things", 91 | "fork": false, 92 | "url": "https://github.com/ocombe/test", 93 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 94 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 95 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 96 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 97 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 98 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 99 | "events_url": "https://api.github.com/repos/ocombe/test/events", 100 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 101 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 102 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 103 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 104 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 105 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 106 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 107 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 108 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 109 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 110 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 111 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 112 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 113 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 114 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 115 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 116 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 117 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 118 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 119 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 120 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 121 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 122 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 123 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 124 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 125 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 126 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 127 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 128 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 129 | "created_at": 1509443542, 130 | "updated_at": "2017-10-31T09:52:22Z", 131 | "pushed_at": 1511970283, 132 | "git_url": "git://github.com/ocombe/test.git", 133 | "ssh_url": "git@github.com:ocombe/test.git", 134 | "clone_url": "https://github.com/ocombe/test.git", 135 | "svn_url": "https://github.com/ocombe/test", 136 | "homepage": null, 137 | "size": 3, 138 | "stargazers_count": 0, 139 | "watchers_count": 0, 140 | "language": null, 141 | "has_issues": true, 142 | "has_projects": true, 143 | "has_downloads": true, 144 | "has_wiki": true, 145 | "has_pages": false, 146 | "forks_count": 0, 147 | "mirror_url": null, 148 | "archived": false, 149 | "open_issues_count": 9, 150 | "license": { 151 | "key": "mit", 152 | "name": "MIT License", 153 | "spdx_id": "MIT", 154 | "url": "https://api.github.com/licenses/mit" 155 | }, 156 | "forks": 0, 157 | "open_issues": 9, 158 | "watchers": 0, 159 | "default_branch": "master", 160 | "stargazers": 0, 161 | "master_branch": "master" 162 | }, 163 | "pusher": { 164 | "name": "ocombe", 165 | "email": "olivier.combe@gmail.com" 166 | }, 167 | "sender": { 168 | "login": "ocombe", 169 | "id": 265378, 170 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 171 | "gravatar_id": "", 172 | "url": "https://api.github.com/users/ocombe", 173 | "html_url": "https://github.com/ocombe", 174 | "followers_url": "https://api.github.com/users/ocombe/followers", 175 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 176 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 177 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 178 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 179 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 180 | "repos_url": "https://api.github.com/users/ocombe/repos", 181 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 182 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 183 | "type": "User", 184 | "site_admin": false 185 | }, 186 | "installation": { 187 | "id": 63922 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/fixtures/status.json: -------------------------------------------------------------------------------- 1 | { 2 | "event": "status", 3 | "name": "status", 4 | "payload": { 5 | "id": 6165533824, 6 | "sha": "271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 7 | "name": "ocombe/test", 8 | "target_url": null, 9 | "context": "ci/angular: merge status", 10 | "description": "Conflicts with base branch \"master\", status \"continuous-integration/travis-ci/push\" is failing, status \"continuous-integration/travis-ci/...", 11 | "state": "failure", 12 | "commit": { 13 | "sha": "271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 14 | "node_id": "MDY6Q29tbWl0MTA4OTc2MjQwOjI3MWNhOGIzZGY1N2VjZWQwZGU5Y2M5OWE5Y2Q1OGY3Y2JmMTdmNjQ=", 15 | "commit": { 16 | "author": { 17 | "name": "Olivier Combe", 18 | "email": "olivier.combe@gmail.com", 19 | "date": "2017-12-04T14:41:51Z" 20 | }, 21 | "committer": { 22 | "name": "GitHub", 23 | "email": "noreply@github.com", 24 | "date": "2017-12-04T14:41:51Z" 25 | }, 26 | "message": "Update .gitignore", 27 | "tree": { 28 | "sha": "7c5aff0d2813db3153c3936f276e3876f9280d61", 29 | "url": "https://api.github.com/repos/ocombe/test/git/trees/7c5aff0d2813db3153c3936f276e3876f9280d61" 30 | }, 31 | "url": "https://api.github.com/repos/ocombe/test/git/commits/271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 32 | "comment_count": 0, 33 | "verification": { 34 | "verified": true, 35 | "reason": "valid", 36 | "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJaJV6vCRBK7hj4Ov3rIwAAdHIIACl+7WBQAQ3NPQayHmw/xlmO\nHjHKHs6nlAJ3jDZYCjilAvOSMeQrpSnfDoj+Nh3U0O+5kxWFDqwHPfb0LKGcN6qK\noS0pdPnEZInpTFlLMQETSjpVe8s54vPsynqRjxtQOhTAsknc7ft6OG+TIeSRSVKT\noE+CkLhXfO81IFh8hVtFVqHJc8xqUQd0XPULNRHg14ZSt9SSDMcLGCRcJTu5nIUc\nwVncDo0/byvu58he0e3rxG4vbpLG3Y/IfI338OZfOoAzEWgq8SJ3CHXPSbTiHaPn\nnxF3MLp6RpdiSYaxvAjCKhsCXbElfE6Sj4YZkE2WGEhpvFNdgDrCBSH+fWkQZxk=\n=Y7l8\n-----END PGP SIGNATURE-----\n", 37 | "payload": "tree 7c5aff0d2813db3153c3936f276e3876f9280d61\nparent 0a2c6dee860d0cf99b03c3f891a432187219080d\nauthor Olivier Combe 1512398511 +0100\ncommitter GitHub 1512398511 +0100\n\nUpdate .gitignore" 38 | } 39 | }, 40 | "url": "https://api.github.com/repos/ocombe/test/commits/271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 41 | "html_url": "https://github.com/ocombe/test/commit/271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 42 | "comments_url": "https://api.github.com/repos/ocombe/test/commits/271ca8b3df57eced0de9cc99a9cd58f7cbf17f64/comments", 43 | "author": { 44 | "login": "ocombe", 45 | "id": 265378, 46 | "node_id": "MDQ6VXNlcjI2NTM3OA==", 47 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 48 | "gravatar_id": "", 49 | "url": "https://api.github.com/users/ocombe", 50 | "html_url": "https://github.com/ocombe", 51 | "followers_url": "https://api.github.com/users/ocombe/followers", 52 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 53 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 54 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 55 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 56 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 57 | "repos_url": "https://api.github.com/users/ocombe/repos", 58 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 59 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 60 | "type": "User", 61 | "site_admin": false 62 | }, 63 | "committer": { 64 | "login": "web-flow", 65 | "id": 19864447, 66 | "node_id": "MDQ6VXNlcjE5ODY0NDQ3", 67 | "avatar_url": "https://avatars3.githubusercontent.com/u/19864447?v=4", 68 | "gravatar_id": "", 69 | "url": "https://api.github.com/users/web-flow", 70 | "html_url": "https://github.com/web-flow", 71 | "followers_url": "https://api.github.com/users/web-flow/followers", 72 | "following_url": "https://api.github.com/users/web-flow/following{/other_user}", 73 | "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", 74 | "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", 75 | "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", 76 | "organizations_url": "https://api.github.com/users/web-flow/orgs", 77 | "repos_url": "https://api.github.com/users/web-flow/repos", 78 | "events_url": "https://api.github.com/users/web-flow/events{/privacy}", 79 | "received_events_url": "https://api.github.com/users/web-flow/received_events", 80 | "type": "User", 81 | "site_admin": false 82 | }, 83 | "parents": [ 84 | { 85 | "sha": "0a2c6dee860d0cf99b03c3f891a432187219080d", 86 | "url": "https://api.github.com/repos/ocombe/test/commits/0a2c6dee860d0cf99b03c3f891a432187219080d", 87 | "html_url": "https://github.com/ocombe/test/commit/0a2c6dee860d0cf99b03c3f891a432187219080d" 88 | } 89 | ] 90 | }, 91 | "branches": [ 92 | { 93 | "name": "ocombe-patch-3", 94 | "commit": { 95 | "sha": "271ca8b3df57eced0de9cc99a9cd58f7cbf17f64", 96 | "url": "https://api.github.com/repos/ocombe/test/commits/271ca8b3df57eced0de9cc99a9cd58f7cbf17f64" 97 | }, 98 | "protected": false 99 | } 100 | ], 101 | "created_at": "2019-01-30T13:56:48+00:00", 102 | "updated_at": "2019-01-30T13:56:48+00:00", 103 | "repository": { 104 | "id": 108976240, 105 | "node_id": "MDEwOlJlcG9zaXRvcnkxMDg5NzYyNDA=", 106 | "name": "test", 107 | "full_name": "ocombe/test", 108 | "private": false, 109 | "owner": { 110 | "login": "ocombe", 111 | "id": 265378, 112 | "node_id": "MDQ6VXNlcjI2NTM3OA==", 113 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 114 | "gravatar_id": "", 115 | "url": "https://api.github.com/users/ocombe", 116 | "html_url": "https://github.com/ocombe", 117 | "followers_url": "https://api.github.com/users/ocombe/followers", 118 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 119 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 120 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 121 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 122 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 123 | "repos_url": "https://api.github.com/users/ocombe/repos", 124 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 125 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 126 | "type": "User", 127 | "site_admin": false 128 | }, 129 | "html_url": "https://github.com/ocombe/test", 130 | "description": "Repository used to test things", 131 | "fork": false, 132 | "url": "https://api.github.com/repos/ocombe/test", 133 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 134 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 135 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 136 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 137 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 138 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 139 | "events_url": "https://api.github.com/repos/ocombe/test/events", 140 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 141 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 142 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 143 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 144 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 145 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 146 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 147 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 148 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 149 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 150 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 151 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 152 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 153 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 154 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 155 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 156 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 157 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 158 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 159 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 160 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 161 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 162 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 163 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 164 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 165 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 166 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 167 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 168 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 169 | "created_at": "2017-10-31T09:52:22Z", 170 | "updated_at": "2019-01-09T13:06:16Z", 171 | "pushed_at": "2019-01-09T13:06:13Z", 172 | "git_url": "git://github.com/ocombe/test.git", 173 | "ssh_url": "git@github.com:ocombe/test.git", 174 | "clone_url": "https://github.com/ocombe/test.git", 175 | "svn_url": "https://github.com/ocombe/test", 176 | "homepage": null, 177 | "size": 105, 178 | "stargazers_count": 0, 179 | "watchers_count": 0, 180 | "language": "JavaScript", 181 | "has_issues": true, 182 | "has_projects": true, 183 | "has_downloads": true, 184 | "has_wiki": true, 185 | "has_pages": false, 186 | "forks_count": 2, 187 | "mirror_url": null, 188 | "archived": false, 189 | "open_issues_count": 25, 190 | "license": { 191 | "key": "mit", 192 | "name": "MIT License", 193 | "spdx_id": "MIT", 194 | "url": "https://api.github.com/licenses/mit", 195 | "node_id": "MDc6TGljZW5zZTEz" 196 | }, 197 | "forks": 2, 198 | "open_issues": 25, 199 | "watchers": 0, 200 | "default_branch": "master" 201 | }, 202 | "sender": { 203 | "login": "wheatley-dev[bot]", 204 | "id": 35229909, 205 | "node_id": "MDM6Qm90MzUyMjk5MDk=", 206 | "avatar_url": "https://avatars2.githubusercontent.com/in/7968?v=4", 207 | "gravatar_id": "", 208 | "url": "https://api.github.com/users/wheatley-dev%5Bbot%5D", 209 | "html_url": "https://github.com/apps/wheatley-dev", 210 | "followers_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/followers", 211 | "following_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/following{/other_user}", 212 | "gists_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/gists{/gist_id}", 213 | "starred_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/starred{/owner}{/repo}", 214 | "subscriptions_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/subscriptions", 215 | "organizations_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/orgs", 216 | "repos_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/repos", 217 | "events_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/events{/privacy}", 218 | "received_events_url": "https://api.github.com/users/wheatley-dev%5Bbot%5D/received_events", 219 | "type": "Bot", 220 | "site_admin": false 221 | }, 222 | "installation": { 223 | "id": 78813, 224 | "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNzg4MTM=" 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /test/merge.spec.ts: -------------------------------------------------------------------------------- 1 | import {Context, Application} from "probot"; 2 | import {MergeTask} from "../functions/src/plugins/merge"; 3 | import {appConfig} from "../functions/src/default"; 4 | import {MockFirestore} from './mocks/firestore'; 5 | import {mockGithub, mockGraphQL} from "./mocks/github"; 6 | import {CommonTask} from "../functions/src/plugins/common"; 7 | import {GitHubAPI} from "probot/lib/github"; 8 | 9 | describe('merge', () => { 10 | let robot: Application; 11 | let github: GitHubAPI; 12 | let commonTask: CommonTask; 13 | let mergeTask: MergeTask; 14 | let store: FirebaseFirestore.Firestore; 15 | 16 | beforeEach(() => { 17 | mockGithub('repos'); 18 | mockGithub('get-installations'); 19 | mockGithub('get-installation-repositories'); 20 | mockGithub('repo-pull-requests'); 21 | mockGithub('repo-pull-request'); 22 | mockGithub('repo-pull-request-reviews'); 23 | mockGithub('repo-pull-request-requested-reviewers'); 24 | 25 | // create the mock Firebase Firestore 26 | store = new MockFirestore(); 27 | 28 | // Create a new Robot to run our plugin 29 | robot = new Application(); 30 | 31 | // Mock out the GitHub API 32 | github = GitHubAPI({ 33 | debug: true, 34 | logger: robot.log 35 | }); 36 | 37 | // Mock out GitHub App authentication and return our mock client 38 | robot.auth = () => Promise.resolve(github); 39 | 40 | // create plugin 41 | mergeTask = new MergeTask(robot, store); 42 | commonTask = new CommonTask(robot, store); 43 | }); 44 | 45 | describe('getConfig', () => { 46 | it('should return the default merge config', async () => { 47 | const event = require('./fixtures/issues.opened.json'); 48 | const context = new Context(event, github, robot.log); 49 | const config = await mergeTask.getConfig(context); 50 | expect(config).toEqual(appConfig.merge); 51 | }); 52 | }); 53 | 54 | describe('init', () => { 55 | it('should work with a manual init', async () => { 56 | await commonTask.manualInit(); 57 | let storeData = await commonTask.repositories.get(); 58 | // shouldn't work if allowInit is false 59 | expect(storeData.docs.length).toEqual(0); 60 | 61 | await commonTask.admin.doc('config').set({allowInit: true}); 62 | await commonTask.manualInit(); 63 | storeData = await commonTask.repositories.get(); 64 | expect(storeData.docs.length).toBeGreaterThan(0); 65 | // our data set in mocks/scenarii/api.github.com/get-installation-repositories.json returns a repository whose name is "test" 66 | expect(storeData.docs[0].data()['name']).toEqual('test'); 67 | }); 68 | 69 | it('should work on repository added', async () => { 70 | const event = require('./fixtures/installation_repositories.added.json'); 71 | const context = new Context(event, github, robot.log); 72 | await commonTask.installInit(context); 73 | const storeData = await commonTask.repositories.get(); 74 | expect(storeData.docs.length).toBeGreaterThan(0); 75 | // our data set in mocks/scenarii/api.github.com/get-installation-repositories.json returns a repository whose name is "test" 76 | expect(storeData.docs[0].data()['name']).toEqual('test'); 77 | }); 78 | 79 | it('should work on app installation', async () => { 80 | const event = require('./fixtures/installation.created.json'); 81 | await commonTask.init(github, event.payload.repositories); 82 | const storeData = await commonTask.pullRequests.get(); 83 | expect(storeData.docs.length).toBeGreaterThan(0); 84 | // our data set in mocks/scenarii/api.github.com/repo-pull-request.json returns a PR whose number value is 1 85 | expect(storeData.docs[0].data()['number']).toEqual(1); 86 | }); 87 | }); 88 | 89 | describe('reviews', () => { 90 | it('should be able to get the accurate number of pending reviews', async () => { 91 | mockGraphQL({ 92 | "repository": { 93 | "pullRequest": { 94 | "number": 19, 95 | "state": "OPEN", 96 | "reviews": { 97 | "nodes": [ 98 | { 99 | "authorAssociation": "COLLABORATOR", 100 | "author": { 101 | "userId": "MDQ6VXNlcjM3MTc1ODEz" 102 | }, 103 | "state": "APPROVED", 104 | "createdAt": "2018-03-20T16:12:02Z" 105 | }, 106 | { 107 | "authorAssociation": "COLLABORATOR", 108 | "author": { 109 | "userId": "MDQ6VXNlcjM3MTc1ODEz" 110 | }, 111 | "state": "APPROVED", 112 | "createdAt": "2018-03-26T12:42:49Z" 113 | }, 114 | { 115 | "authorAssociation": "COLLABORATOR", 116 | "author": { 117 | "userId": "MDQ6VXNlcjM3MTc1ODEz" 118 | }, 119 | "state": "APPROVED", 120 | "createdAt": "2018-03-26T14:02:37Z" 121 | } 122 | ] 123 | }, 124 | "reviewRequests": { 125 | "nodes": [] 126 | } 127 | } 128 | } 129 | }); 130 | // const event = require('./fixtures/pr-comments.json'); 131 | const event = require('./fixtures/pull_request_review.submitted.json'); 132 | const context = new Context(event, github, robot.log) as any; 133 | const pendingReviews = await mergeTask.getPendingReviews(context, context.payload.pull_request); 134 | expect(pendingReviews).toEqual(0); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/mocks/database.ts: -------------------------------------------------------------------------------- 1 | export class MockDatabaseHost { 2 | values = new Map(); 3 | 4 | database() { 5 | return { 6 | ref: (path: string) => { 7 | return { 8 | once: (_: string) => { 9 | return Promise.resolve({ 10 | val: () => this.values.get(path) 11 | }); 12 | }, 13 | update: (value: any) => { 14 | this.values.set(path, value); 15 | }, 16 | then: (fct: Function) => fct(this.values.get(path)) 17 | }; 18 | } 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/mocks/firestore.ts: -------------------------------------------------------------------------------- 1 | class WriteResult implements FirebaseFirestore.WriteResult { 2 | writeTime: FirebaseFirestore.Timestamp; 3 | 4 | constructor() { 5 | this.writeTime = new Date().getTime() as any as FirebaseFirestore.Timestamp; 6 | } 7 | 8 | isEqual(other: FirebaseFirestore.WriteResult): boolean { 9 | throw new Error("Method not implemented."); 10 | } 11 | } 12 | 13 | class DocumentSnapshot implements FirebaseFirestore.DocumentSnapshot { 14 | exists: boolean; 15 | ref: FirebaseFirestore.DocumentReference; 16 | id: string; 17 | createTime?: FirebaseFirestore.Timestamp; 18 | updateTime?: FirebaseFirestore.Timestamp; 19 | readTime: FirebaseFirestore.Timestamp; 20 | private _data: any; 21 | 22 | constructor(data?: any) { 23 | if(data) { 24 | this._data = data; 25 | this.exists = true; 26 | } 27 | } 28 | 29 | data(): FirebaseFirestore.DocumentData { 30 | return this._data; 31 | } 32 | 33 | get(fieldPath: string | FirebaseFirestore.FieldPath) { 34 | throw new Error("Method not implemented."); 35 | } 36 | 37 | isEqual(other: FirebaseFirestore.DocumentSnapshot): boolean { 38 | throw new Error("Method not implemented."); 39 | } 40 | } 41 | 42 | class DocumentReference implements FirebaseFirestore.DocumentReference { 43 | id: string; 44 | firestore: FirebaseFirestore.Firestore; 45 | parent: FirebaseFirestore.CollectionReference; 46 | path: string; 47 | private _data: any; 48 | 49 | constructor(firestore: FirebaseFirestore.Firestore, path: string) { 50 | this.firestore = firestore; 51 | this.path = path; 52 | } 53 | 54 | collection(collectionPath: string): FirebaseFirestore.CollectionReference { 55 | throw new Error("Method not implemented."); 56 | } 57 | 58 | getCollections(): Promise { 59 | throw new Error("Method not implemented."); 60 | } 61 | 62 | create(data: FirebaseFirestore.DocumentData): Promise { 63 | throw new Error("Method not implemented."); 64 | } 65 | 66 | set(data: FirebaseFirestore.DocumentData, options?: FirebaseFirestore.SetOptions): Promise { 67 | this._data = data; 68 | return Promise.resolve(new WriteResult()); 69 | } 70 | 71 | update(data: any, precondition?: any, ...rest: any[]): Promise { 72 | throw new Error("Method not implemented."); 73 | } 74 | 75 | delete(precondition?: FirebaseFirestore.Precondition): Promise { 76 | throw new Error("Method not implemented."); 77 | } 78 | 79 | get(): Promise { 80 | return Promise.resolve(new DocumentSnapshot(this._data)); 81 | } 82 | 83 | onSnapshot(onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: Error) => void): () => void { 84 | throw new Error("Method not implemented."); 85 | } 86 | 87 | isEqual(other: FirebaseFirestore.DocumentReference): boolean { 88 | throw new Error("Method not implemented."); 89 | } 90 | 91 | listCollections(): Promise { 92 | throw new Error("Method not implemented."); 93 | } 94 | } 95 | 96 | class QueryDocumentSnapshot implements FirebaseFirestore.QueryDocumentSnapshot { 97 | createTime: FirebaseFirestore.Timestamp; 98 | updateTime: FirebaseFirestore.Timestamp; 99 | 100 | constructor(private doc: FirebaseFirestore.DocumentSnapshot) { 101 | } 102 | 103 | data(): FirebaseFirestore.DocumentData { 104 | return this.doc.data(); 105 | } 106 | 107 | exists: boolean; 108 | ref: FirebaseFirestore.DocumentReference; 109 | id: string; 110 | readTime: FirebaseFirestore.Timestamp; 111 | 112 | get(fieldPath: string | FirebaseFirestore.FieldPath) { 113 | return this.data(); 114 | } 115 | 116 | isEqual(other: FirebaseFirestore.DocumentSnapshot): boolean { 117 | throw new Error("Method not implemented."); 118 | } 119 | } 120 | 121 | class QuerySnapshot implements FirebaseFirestore.QuerySnapshot { 122 | query: FirebaseFirestore.Query; 123 | docChanges: () => FirebaseFirestore.DocumentChange[]; 124 | docs: QueryDocumentSnapshot[]; 125 | size: number; 126 | empty: boolean; 127 | readTime: FirebaseFirestore.Timestamp; 128 | 129 | constructor(query: Query, docs: QueryDocumentSnapshot[]) { 130 | this.query = query; 131 | this.docs = docs; 132 | } 133 | 134 | forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: any): void { 135 | this.docs.forEach(callback); 136 | } 137 | 138 | isEqual(other: FirebaseFirestore.QuerySnapshot): boolean { 139 | throw new Error("Method not implemented."); 140 | } 141 | } 142 | 143 | class Query implements FirebaseFirestore.Query { 144 | firestore: FirebaseFirestore.Firestore; 145 | collection: FirebaseFirestore.CollectionReference; 146 | 147 | constructor(firestore: FirebaseFirestore.Firestore, collection: FirebaseFirestore.CollectionReference) { 148 | this.firestore = firestore; 149 | this.collection = collection; 150 | } 151 | 152 | where(fieldPath: string | FirebaseFirestore.FieldPath, opStr: FirebaseFirestore.WhereFilterOp, value: any): FirebaseFirestore.Query { 153 | return this; 154 | } 155 | 156 | orderBy(fieldPath: string | FirebaseFirestore.FieldPath, directionStr?: FirebaseFirestore.OrderByDirection): FirebaseFirestore.Query { 157 | return this; 158 | } 159 | 160 | limit(limit: number): FirebaseFirestore.Query { 161 | return this; 162 | } 163 | 164 | offset(offset: number): FirebaseFirestore.Query { 165 | return this; 166 | } 167 | 168 | select(...field: (string | FirebaseFirestore.FieldPath)[]): FirebaseFirestore.Query { 169 | return this; 170 | } 171 | 172 | startAt(...fieldValues: any[]): FirebaseFirestore.Query { 173 | return this; 174 | } 175 | 176 | startAfter(...fieldValues: any[]): FirebaseFirestore.Query { 177 | return this; 178 | } 179 | 180 | endBefore(...fieldValues: any[]): FirebaseFirestore.Query { 181 | return this; 182 | } 183 | 184 | endAt(...fieldValues: any[]): FirebaseFirestore.Query { 185 | return this; 186 | } 187 | 188 | get(): Promise { 189 | return this.collection.get(); 190 | } 191 | 192 | stream(): NodeJS.ReadableStream { 193 | throw new Error("Method not implemented."); 194 | } 195 | 196 | onSnapshot(onNext: (snapshot: FirebaseFirestore.QuerySnapshot) => void, onError?: (error: Error) => void): () => void { 197 | throw new Error("Method not implemented."); 198 | } 199 | 200 | isEqual(other: FirebaseFirestore.Query): boolean { 201 | throw new Error("Method not implemented."); 202 | } 203 | } 204 | 205 | class Collection implements FirebaseFirestore.CollectionReference { 206 | firestore: FirebaseFirestore.Firestore; 207 | id: string; 208 | parent: FirebaseFirestore.DocumentReference; 209 | path: string; 210 | private _documents = new Map(); 211 | 212 | constructor(firestore: FirebaseFirestore.Firestore, path: string) { 213 | this.path = path; 214 | this.firestore = firestore; 215 | } 216 | 217 | doc(documentPath?: string): FirebaseFirestore.DocumentReference { 218 | if(this._documents.has(documentPath)) { 219 | return this._documents.get(documentPath); 220 | } 221 | const doc = new DocumentReference(this.firestore, documentPath); 222 | this._documents.set(documentPath, doc); 223 | return doc; 224 | } 225 | 226 | add(data: FirebaseFirestore.DocumentData): Promise { 227 | throw new Error("Method not implemented."); 228 | } 229 | 230 | 231 | where(fieldPath: string | FirebaseFirestore.FieldPath, opStr: FirebaseFirestore.WhereFilterOp, value: any): FirebaseFirestore.Query { 232 | return new Query(this.firestore, this); 233 | } 234 | 235 | orderBy(fieldPath: string | FirebaseFirestore.FieldPath, directionStr?: FirebaseFirestore.OrderByDirection): FirebaseFirestore.Query { 236 | throw new Error("Method not implemented."); 237 | } 238 | 239 | limit(limit: number): FirebaseFirestore.Query { 240 | throw new Error("Method not implemented."); 241 | } 242 | 243 | offset(offset: number): FirebaseFirestore.Query { 244 | throw new Error("Method not implemented."); 245 | } 246 | 247 | select(...field: (string | FirebaseFirestore.FieldPath)[]): FirebaseFirestore.Query { 248 | throw new Error("Method not implemented."); 249 | } 250 | 251 | startAt(...fieldValues: any[]): FirebaseFirestore.Query { 252 | throw new Error("Method not implemented."); 253 | } 254 | 255 | startAfter(...fieldValues: any[]): FirebaseFirestore.Query { 256 | throw new Error("Method not implemented."); 257 | } 258 | 259 | endBefore(...fieldValues: any[]): FirebaseFirestore.Query { 260 | throw new Error("Method not implemented."); 261 | } 262 | 263 | endAt(...fieldValues: any[]): FirebaseFirestore.Query { 264 | throw new Error("Method not implemented."); 265 | } 266 | 267 | async get(): Promise { 268 | const snapshots: QueryDocumentSnapshot[] = await Promise.all( 269 | Array.from(this._documents.values()) 270 | .map(async (doc: FirebaseFirestore.DocumentReference) => { 271 | return new QueryDocumentSnapshot(await doc.get()); 272 | }) 273 | ); 274 | 275 | return Promise.resolve(new QuerySnapshot(new Query(this.firestore, this), snapshots)); 276 | } 277 | 278 | stream(): NodeJS.ReadableStream { 279 | throw new Error("Method not implemented."); 280 | } 281 | 282 | onSnapshot(onNext: (snapshot: FirebaseFirestore.QuerySnapshot) => void, onError?: (error: Error) => void): () => void { 283 | throw new Error("Method not implemented."); 284 | } 285 | 286 | isEqual(other: FirebaseFirestore.CollectionReference): boolean { 287 | throw new Error("Method not implemented."); 288 | } 289 | 290 | listDocuments(): Promise { 291 | throw new Error("Method not implemented."); 292 | } 293 | } 294 | 295 | export class MockFirestore implements FirebaseFirestore.Firestore { 296 | private _collections = new Map(); 297 | 298 | settings(settings: FirebaseFirestore.Settings): void { 299 | throw new Error("Method not implemented."); 300 | } 301 | 302 | collection(collectionPath: string): FirebaseFirestore.CollectionReference { 303 | if(this._collections.has(collectionPath)) { 304 | return this._collections.get(collectionPath); 305 | } 306 | const collection = new Collection(this, collectionPath); 307 | this._collections.set(collectionPath, collection); 308 | return collection; 309 | } 310 | 311 | doc(documentPath: string): FirebaseFirestore.DocumentReference { 312 | throw new Error("Method not implemented."); 313 | } 314 | 315 | getAll(...documentRef: FirebaseFirestore.DocumentReference[]): Promise { 316 | throw new Error("Method not implemented."); 317 | } 318 | 319 | getCollections(): Promise { 320 | throw new Error("Method not implemented."); 321 | } 322 | 323 | runTransaction(updateFunction: (transaction: FirebaseFirestore.Transaction) => Promise): Promise { 324 | throw new Error("Method not implemented."); 325 | } 326 | 327 | batch(): FirebaseFirestore.WriteBatch { 328 | throw new Error("Method not implemented."); 329 | } 330 | 331 | listCollections(): Promise { 332 | throw new Error("Method not implemented."); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /test/mocks/github.ts: -------------------------------------------------------------------------------- 1 | import {define, emitter, loadDefs, NockDefinition} from 'nock'; 2 | const nock = require('nock'); 3 | 4 | function get(name: string): NockDefinition[] { 5 | return loadDefs(`${__dirname}/scenarii/api.github.com/${name}.json`); 6 | } 7 | 8 | function objectToRawHeaders(map: any) { 9 | const keys = Object.keys(map).sort(); 10 | return [].concat.apply([], keys.map(key => [key, map[key]])); 11 | } 12 | 13 | export function mockGithub(name: string) { 14 | const fixtures = get(name); 15 | fixtures.forEach((fixture: any) => { 16 | fixture['rawHeaders'] = objectToRawHeaders(fixture.headers); 17 | delete fixture.headers; 18 | }); 19 | define(fixtures); 20 | } 21 | 22 | export function mockGraphQL(response: {[key: string]: any}) { 23 | nock('https://api.github.com:443', {"encodedQueryParams": true}) 24 | .post('/graphql') 25 | .reply(200, { 26 | "data": response 27 | }); 28 | } 29 | 30 | emitter.on('no match', function(req, options, body) { 31 | console.log(options, body); 32 | }); 33 | -------------------------------------------------------------------------------- /test/mocks/http.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpOptions } from "../../functions/src/http"; 2 | 3 | export class MockHttpHost { 4 | private endpoints: {[url: string]: { 5 | hits: number; 6 | response: any; 7 | }} = {}; 8 | registerEndpoint(url: string, response: any) { 9 | this.endpoints[url] = { 10 | response, 11 | hits: 0, 12 | }; 13 | } 14 | 15 | getHits(url: string) { 16 | return this.endpoints[url].hits; 17 | } 18 | 19 | httpClient(): HttpClient { 20 | return { 21 | get: (url: string, options: HttpOptions): Promise => { 22 | this.endpoints[url].hits ++; 23 | return new Promise((resolve, reject) => resolve(this.endpoints[url].response as string)); 24 | } 25 | } as HttpClient; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/get-installation-repositories.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/installation/repositories", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "repositories": [ 10 | { 11 | "id": 108976240, 12 | "name": "test", 13 | "full_name": "ocombe/test", 14 | "owner": { 15 | "login": "ocombe", 16 | "id": 265378, 17 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 18 | "gravatar_id": "", 19 | "url": "https://api.github.com/users/ocombe", 20 | "html_url": "https://github.com/ocombe", 21 | "followers_url": "https://api.github.com/users/ocombe/followers", 22 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 23 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 24 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 25 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 26 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 27 | "repos_url": "https://api.github.com/users/ocombe/repos", 28 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 29 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 30 | "type": "User", 31 | "site_admin": false 32 | }, 33 | "private": false, 34 | "html_url": "https://github.com/ocombe/test", 35 | "description": "Repository used to test things", 36 | "fork": false, 37 | "url": "https://api.github.com/repos/ocombe/test", 38 | "forks_url": "https://api.github.com/repos/ocombe/test/forks", 39 | "keys_url": "https://api.github.com/repos/ocombe/test/keys{/key_id}", 40 | "collaborators_url": "https://api.github.com/repos/ocombe/test/collaborators{/collaborator}", 41 | "teams_url": "https://api.github.com/repos/ocombe/test/teams", 42 | "hooks_url": "https://api.github.com/repos/ocombe/test/hooks", 43 | "issue_events_url": "https://api.github.com/repos/ocombe/test/issues/events{/number}", 44 | "events_url": "https://api.github.com/repos/ocombe/test/events", 45 | "assignees_url": "https://api.github.com/repos/ocombe/test/assignees{/user}", 46 | "branches_url": "https://api.github.com/repos/ocombe/test/branches{/branch}", 47 | "tags_url": "https://api.github.com/repos/ocombe/test/tags", 48 | "blobs_url": "https://api.github.com/repos/ocombe/test/git/blobs{/sha}", 49 | "git_tags_url": "https://api.github.com/repos/ocombe/test/git/tags{/sha}", 50 | "git_refs_url": "https://api.github.com/repos/ocombe/test/git/refs{/sha}", 51 | "trees_url": "https://api.github.com/repos/ocombe/test/git/trees{/sha}", 52 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/{sha}", 53 | "languages_url": "https://api.github.com/repos/ocombe/test/languages", 54 | "stargazers_url": "https://api.github.com/repos/ocombe/test/stargazers", 55 | "contributors_url": "https://api.github.com/repos/ocombe/test/contributors", 56 | "subscribers_url": "https://api.github.com/repos/ocombe/test/subscribers", 57 | "subscription_url": "https://api.github.com/repos/ocombe/test/subscription", 58 | "commits_url": "https://api.github.com/repos/ocombe/test/commits{/sha}", 59 | "git_commits_url": "https://api.github.com/repos/ocombe/test/git/commits{/sha}", 60 | "comments_url": "https://api.github.com/repos/ocombe/test/comments{/number}", 61 | "issue_comment_url": "https://api.github.com/repos/ocombe/test/issues/comments{/number}", 62 | "contents_url": "https://api.github.com/repos/ocombe/test/contents/{+path}", 63 | "compare_url": "https://api.github.com/repos/ocombe/test/compare/{base}...{head}", 64 | "merges_url": "https://api.github.com/repos/ocombe/test/merges", 65 | "archive_url": "https://api.github.com/repos/ocombe/test/{archive_format}{/ref}", 66 | "downloads_url": "https://api.github.com/repos/ocombe/test/downloads", 67 | "issues_url": "https://api.github.com/repos/ocombe/test/issues{/number}", 68 | "pulls_url": "https://api.github.com/repos/ocombe/test/pulls{/number}", 69 | "milestones_url": "https://api.github.com/repos/ocombe/test/milestones{/number}", 70 | "notifications_url": "https://api.github.com/repos/ocombe/test/notifications{?since,all,participating}", 71 | "labels_url": "https://api.github.com/repos/ocombe/test/labels{/name}", 72 | "releases_url": "https://api.github.com/repos/ocombe/test/releases{/id}", 73 | "deployments_url": "https://api.github.com/repos/ocombe/test/deployments", 74 | "created_at": "2017-10-31T09:52:22Z", 75 | "updated_at": "2017-10-31T09:52:22Z", 76 | "pushed_at": "2017-11-29T17:48:58Z", 77 | "git_url": "git://github.com/ocombe/test.git", 78 | "ssh_url": "git@github.com:ocombe/test.git", 79 | "clone_url": "https://github.com/ocombe/test.git", 80 | "svn_url": "https://github.com/ocombe/test", 81 | "homepage": null, 82 | "size": 7, 83 | "stargazers_count": 0, 84 | "watchers_count": 0, 85 | "language": null, 86 | "has_issues": true, 87 | "has_projects": true, 88 | "has_downloads": true, 89 | "has_wiki": true, 90 | "has_pages": false, 91 | "forks_count": 0, 92 | "mirror_url": null, 93 | "archived": false, 94 | "open_issues_count": 9, 95 | "license": { 96 | "key": "mit", 97 | "name": "MIT License", 98 | "spdx_id": "MIT", 99 | "url": "https://api.github.com/licenses/mit" 100 | }, 101 | "forks": 0, 102 | "open_issues": 9, 103 | "watchers": 0, 104 | "default_branch": "master" 105 | } 106 | ] 107 | }, 108 | "reqheaders": { 109 | "accept": "application/vnd.github.machine-man-preview+json", 110 | "host": "api.github.com" 111 | }, 112 | "headers": { 113 | "access-control-allow-origin": "*", 114 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 115 | "cache-control": "public, max-age=60, s-maxage=60", 116 | "connection": "close", 117 | "content-length": "906", 118 | "content-security-policy": "default-src 'none'", 119 | "content-type": "application/json; charset=utf-8", 120 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 121 | "etag": "\"00000000000000000000000000000000\"", 122 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 123 | "status": "200 OK", 124 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 125 | "x-content-type-options": "nosniff", 126 | "x-frame-options": "deny", 127 | "x-github-media-type": "github.v3; format=json", 128 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 129 | "x-ratelimit-limit": "60", 130 | "x-ratelimit-remaining": "59", 131 | "x-ratelimit-reset": "1507651200000", 132 | "x-runtime-rack": "0.000000", 133 | "x-xss-protection": "1; mode=block" 134 | }, 135 | "badheaders": [ 136 | "authorization" 137 | ] 138 | } 139 | ] 140 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/get-installations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/app/installations", 6 | "body": "", 7 | "status": 200, 8 | "response": [ 9 | { 10 | "id": 63922, 11 | "account": { 12 | "login": "ocombe", 13 | "id": 265378, 14 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 15 | "gravatar_id": "", 16 | "url": "https://api.github.com/users/ocombe", 17 | "html_url": "https://github.com/ocombe", 18 | "followers_url": "https://api.github.com/users/ocombe/followers", 19 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 20 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 21 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 22 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 23 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 24 | "repos_url": "https://api.github.com/users/ocombe/repos", 25 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 26 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 27 | "type": "User", 28 | "site_admin": false 29 | }, 30 | "repository_selection": "selected", 31 | "access_tokens_url": "https://api.github.com/installations/63922/access_tokens", 32 | "repositories_url": "https://api.github.com/installation/repositories", 33 | "html_url": "https://github.com/settings/installations/63922", 34 | "app_id": 6359, 35 | "target_id": 265378, 36 | "target_type": "User", 37 | "permissions": { 38 | "pull_requests": "write", 39 | "issues": "write", 40 | "statuses": "write", 41 | "contents": "read", 42 | "metadata": "read" 43 | }, 44 | "events": [ 45 | "issues", 46 | "issue_comment", 47 | "pull_request", 48 | "pull_request_review", 49 | "pull_request_review_comment", 50 | "push", 51 | "status" 52 | ], 53 | "created_at": "2017-10-31T09:58:10Z", 54 | "updated_at": "2017-12-05T14:23:31Z", 55 | "single_file_name": null 56 | } 57 | ], 58 | "reqheaders": { 59 | "accept": "application/vnd.github.machine-man-preview+json", 60 | "host": "api.github.com" 61 | }, 62 | "headers": { 63 | "access-control-allow-origin": "*", 64 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 65 | "cache-control": "public, max-age=60, s-maxage=60", 66 | "connection": "close", 67 | "content-length": "906", 68 | "content-security-policy": "default-src 'none'", 69 | "content-type": "application/json; charset=utf-8", 70 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 71 | "etag": "\"00000000000000000000000000000000\"", 72 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 73 | "status": "200 OK", 74 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 75 | "x-content-type-options": "nosniff", 76 | "x-frame-options": "deny", 77 | "x-github-media-type": "github.v3; format=json", 78 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 79 | "x-ratelimit-limit": "60", 80 | "x-ratelimit-remaining": "59", 81 | "x-ratelimit-reset": "1507651200000", 82 | "x-runtime-rack": "0.000000", 83 | "x-xss-protection": "1; mode=block" 84 | }, 85 | "badheaders": [ 86 | "authorization" 87 | ] 88 | } 89 | ] 90 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/repo-pull-request-requested-reviewers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/ocombe/test/pulls/35/requested_reviewers", 6 | "body": "", 7 | "status": 200, 8 | "response": [], 9 | "reqheaders": { 10 | "accept": "application/vnd.github.v3+json", 11 | "host": "api.github.com" 12 | }, 13 | "headers": { 14 | "access-control-allow-origin": "*", 15 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 16 | "cache-control": "public, max-age=60, s-maxage=60", 17 | "connection": "close", 18 | "content-length": "906", 19 | "content-security-policy": "default-src 'none'", 20 | "content-type": "application/json; charset=utf-8", 21 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 22 | "etag": "\"00000000000000000000000000000000\"", 23 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 24 | "status": "200 OK", 25 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 26 | "x-content-type-options": "nosniff", 27 | "x-frame-options": "deny", 28 | "x-github-media-type": "github.v3; format=json", 29 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 30 | "x-ratelimit-limit": "60", 31 | "x-ratelimit-remaining": "59", 32 | "x-ratelimit-reset": "1507651200000", 33 | "x-runtime-rack": "0.000000", 34 | "x-xss-protection": "1; mode=block" 35 | }, 36 | "badheaders": [ 37 | "authorization" 38 | ] 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/repo-pull-request-reviews.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/ocombe/test/pulls/35/reviews?per_page=100", 6 | "body": "", 7 | "status": 200, 8 | "response": [], 9 | "reqheaders": { 10 | "accept": "application/vnd.github.v3+json", 11 | "host": "api.github.com" 12 | }, 13 | "headers": { 14 | "access-control-allow-origin": "*", 15 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 16 | "cache-control": "public, max-age=60, s-maxage=60", 17 | "connection": "close", 18 | "content-length": "906", 19 | "content-security-policy": "default-src 'none'", 20 | "content-type": "application/json; charset=utf-8", 21 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 22 | "etag": "\"00000000000000000000000000000000\"", 23 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 24 | "status": "200 OK", 25 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 26 | "x-content-type-options": "nosniff", 27 | "x-frame-options": "deny", 28 | "x-github-media-type": "github.v3; format=json", 29 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 30 | "x-ratelimit-limit": "60", 31 | "x-ratelimit-remaining": "59", 32 | "x-ratelimit-reset": "1507651200000", 33 | "x-runtime-rack": "0.000000", 34 | "x-xss-protection": "1; mode=block" 35 | }, 36 | "badheaders": [ 37 | "authorization" 38 | ] 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/repo-pull-requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/ocombe/test/pulls?state=open&per_page=100", 6 | "body": "", 7 | "status": 200, 8 | "response": [ 9 | { 10 | "url": "https://api.github.com/repos/ocombe/test/pulls/1", 11 | "id": 156315571, 12 | "html_url": "https://github.com/ocombe/test/pull/1", 13 | "diff_url": "https://github.com/ocombe/test/pull/1.diff", 14 | "patch_url": "https://github.com/ocombe/test/pull/1.patch", 15 | "issue_url": "https://api.github.com/repos/ocombe/test/issues/1", 16 | "number": 1, 17 | "state": "open", 18 | "locked": false, 19 | "title": "Update .gitignore", 20 | "user": { 21 | "login": "ocombe", 22 | "id": 265378, 23 | "avatar_url": "https://avatars0.githubusercontent.com/u/265378?v=4", 24 | "gravatar_id": "", 25 | "url": "https://api.github.com/users/ocombe", 26 | "html_url": "https://github.com/ocombe", 27 | "followers_url": "https://api.github.com/users/ocombe/followers", 28 | "following_url": "https://api.github.com/users/ocombe/following{/other_user}", 29 | "gists_url": "https://api.github.com/users/ocombe/gists{/gist_id}", 30 | "starred_url": "https://api.github.com/users/ocombe/starred{/owner}{/repo}", 31 | "subscriptions_url": "https://api.github.com/users/ocombe/subscriptions", 32 | "organizations_url": "https://api.github.com/users/ocombe/orgs", 33 | "repos_url": "https://api.github.com/users/ocombe/repos", 34 | "events_url": "https://api.github.com/users/ocombe/events{/privacy}", 35 | "received_events_url": "https://api.github.com/users/ocombe/received_events", 36 | "type": "User", 37 | "site_admin": false 38 | }, 39 | "body": "", 40 | "created_at": "2017-12-04T21:50:14Z", 41 | "updated_at": "2017-12-05T1:18:20Z", 42 | "closed_at": null, 43 | "merged_at": null, 44 | "merge_commit_sha": "abdd89bb37f735e05834137852172a539737295d", 45 | "assignee": null, 46 | "assignees": [], 47 | "requested_reviewers": [], 48 | "milestone": null, 49 | "commits_url": "https://api.github.com/repos/ocombe/test/pulls/1/commits", 50 | "review_comments_url": "https://api.github.com/repos/ocombe/test/pulls/1/comments", 51 | "review_comment_url": "https://api.github.com/repos/ocombe/test/pulls/comments{/number}", 52 | "comments_url": "https://api.github.com/repos/ocombe/test/issues/1/comments", 53 | "statuses_url": "https://api.github.com/repos/ocombe/test/statuses/fd8c7aac3e83cd2ac4c6d80ddbb52126af96412c", 54 | "head": { 55 | "label": "ocombe:ocombe-patch-7", 56 | "ref": "ocombe-patch-7", 57 | "sha": "fd8c7aac3e83cd2ac4c6d80ddbb52126af96412c", 58 | "user": {}, 59 | "repo": {} 60 | }, 61 | "base": { 62 | "label": "ocombe:master", 63 | "ref": "master", 64 | "sha": "0a2c6dee860d0cf99b03c3f891a432187219080d", 65 | "user": {}, 66 | "repo": {} 67 | }, 68 | "_links": { 69 | "self": {}, 70 | "html": {}, 71 | "issue": {}, 72 | "comments": {}, 73 | "review_comments": {}, 74 | "review_comment": {}, 75 | "commits": {}, 76 | "statuses": {} 77 | }, 78 | "author_association": "OWNER", 79 | "merged": false, 80 | "mergeable": false, 81 | "rebaseable": false, 82 | "mergeable_state": "dirty", 83 | "merged_by": null, 84 | "comments": 4, 85 | "review_comments": 0, 86 | "maintainer_can_modify": false, 87 | "commits": 1, 88 | "additions": 0, 89 | "deletions": 1, 90 | "changed_files": 1 91 | } 92 | ], 93 | "reqheaders": { 94 | "accept": "application/vnd.github.v3+json", 95 | "host": "api.github.com" 96 | }, 97 | "headers": { 98 | "access-control-allow-origin": "*", 99 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 100 | "cache-control": "public, max-age=60, s-maxage=60", 101 | "connection": "close", 102 | "content-length": "906", 103 | "content-security-policy": "default-src 'none'", 104 | "content-type": "application/json; charset=utf-8", 105 | "date": "Tue, 10 Oct 2017 1:00:00 GMT", 106 | "etag": "\"00000000000000000000000000000000\"", 107 | "last-modified": "Tue, 10 Oct 2017 1:00:00 GMT", 108 | "status": "200 OK", 109 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 110 | "x-content-type-options": "nosniff", 111 | "x-frame-options": "deny", 112 | "x-github-media-type": "github.v3; format=json", 113 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 114 | "x-ratelimit-limit": "60", 115 | "x-ratelimit-remaining": "59", 116 | "x-ratelimit-reset": "1507651200000", 117 | "x-runtime-rack": "0.000000", 118 | "x-xss-protection": "1; mode=block" 119 | }, 120 | "badheaders": [ 121 | "authorization" 122 | ] 123 | } 124 | ] 125 | -------------------------------------------------------------------------------- /test/mocks/scenarii/api.github.com/repos.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "get", 5 | "path": "/repos/ocombe/test/contents/.github/angular-robot.yml", 6 | "body": "", 7 | "status": 200, 8 | "response": { 9 | "name": "angular-robot.yml", 10 | "path": ".github", 11 | "sha": "93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 12 | "size": 13, 13 | "url": "https://api.github.com/repos/ocombe/test/contents/.github/angular-robot.yml?ref=master", 14 | "html_url": "https://github.com/ocombe/test/blob/master/.github/angular-robot.yml", 15 | "git_url": "https://api.github.com/repos/ocombe/test/git/blobs/93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 16 | "download_url": "https://raw.githubusercontent.com/ocombe/test/master/.github/angular-robot.yml", 17 | "type": "file", 18 | "_links": { 19 | "self": "https://api.github.com/repos/ocombe/test/contents/.github/angular-robot.yml?ref=master", 20 | "git": "https://api.github.com/repos/ocombe/test/git/blobs/93a078d1c3f76aa1ca11def8f882a06df1d4a01b", 21 | "html": "https://github.com/ocombe/test/blob/master/.github/angular-robot.yml" 22 | }, 23 | "content": "" 24 | }, 25 | "reqheaders": { 26 | "accept": "application/vnd.github.v3+json", 27 | "host": "api.github.com" 28 | }, 29 | "headers": { 30 | "access-control-allow-origin": "*", 31 | "access-control-expose-headers": "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 32 | "cache-control": "public, max-age=60, s-maxage=60", 33 | "connection": "close", 34 | "content-length": "836", 35 | "content-security-policy": "default-src 'none'", 36 | "content-type": "application/json; charset=utf-8", 37 | "date": "Tue, 10 Oct 2017 16:00:00 GMT", 38 | "etag": "\"00000000000000000000000000000000\"", 39 | "last-modified": "Tue, 10 Oct 2017 16:00:00 GMT", 40 | "status": "200 OK", 41 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 42 | "x-content-type-options": "nosniff", 43 | "x-frame-options": "deny", 44 | "x-github-media-type": "github.v3; format=json", 45 | "x-github-request-id": "0000:00000:0000000:0000000:00000000", 46 | "x-ratelimit-limit": "60", 47 | "x-ratelimit-remaining": "59", 48 | "x-ratelimit-reset": "1507651200000", 49 | "x-runtime-rack": "0.000000", 50 | "x-xss-protection": "1; mode=block" 51 | }, 52 | "badheaders": [ 53 | "authorization" 54 | ] 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /test/triage.spec.ts: -------------------------------------------------------------------------------- 1 | import {Context, Application} from "probot"; 2 | import {GitHubAPI} from "probot/lib/github"; 3 | import {TriageTask} from "../functions/src/plugins/triage"; 4 | import {appConfig} from "../functions/src/default"; 5 | import {MockFirestore} from './mocks/firestore'; 6 | import {mockGithub} from "./mocks/github"; 7 | 8 | describe('triage', () => { 9 | let robot: Application; 10 | let github: GitHubAPI; 11 | let triageTask: TriageTask; 12 | let store: FirebaseFirestore.Firestore; 13 | 14 | beforeEach(() => { 15 | mockGithub('repos'); 16 | 17 | // create the mock Firebase Firestore 18 | store = new MockFirestore(); 19 | 20 | // Create a new Robot to run our plugin 21 | robot = new Application(); 22 | 23 | // Mock out the GitHub API 24 | github = GitHubAPI({ 25 | debug: true, 26 | logger: robot.log 27 | }); 28 | 29 | // Mock out GitHub App authentication and return our mock client 30 | robot.auth = () => Promise.resolve(github); 31 | 32 | // create plugin 33 | triageTask = new TriageTask(robot, store); 34 | }); 35 | 36 | describe('getConfig', () => { 37 | it('should return the default merge config', async () => { 38 | const event = require('./fixtures/issues.opened.json'); 39 | const context = new Context(event, github, robot.log); 40 | const config = await triageTask.getConfig(context); 41 | expect(config).toEqual(appConfig.triage); 42 | }); 43 | }); 44 | 45 | describe('isTriaged', () => { 46 | it('should return the triage status', async () => { 47 | const event = require('./fixtures/issues.labeled.json'); 48 | const context = new Context(event, github, robot.log); 49 | const config = await triageTask.getConfig(context); 50 | 51 | let isTriaged = triageTask.isTriaged(config.l2TriageLabels, ['comp: aio']); 52 | expect(isTriaged).toBeFalsy(); 53 | 54 | isTriaged = triageTask.isTriaged(config.l2TriageLabels, ['comp: aio', 'type: feature']); 55 | expect(isTriaged).toBeTruthy(); 56 | 57 | isTriaged = triageTask.isTriaged(config.l2TriageLabels, ['comp: common', 'type: bug']); 58 | expect(isTriaged).toBeFalsy(); 59 | 60 | isTriaged = triageTask.isTriaged(config.l2TriageLabels, ['comp: common/http', 'type: bug/fix', 'freq1: low', 'severity3: broken']); 61 | expect(isTriaged).toBeTruthy(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es2017"], /* Specify library files to be included in the compilation: */ 7 | "allowJs": true, /* Allow javascript files to be compiled. */ 8 | "declaration": false, /* Generates corresponding '.d.ts' file. */ 9 | "sourceMap": false, /* Generates corresponding '.map' file. */ 10 | "noImplicitAny": true, 11 | 12 | /* Module Resolution Options */ 13 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "noUnusedLocals": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": [true, "allow-declarations"], 14 | 15 | // Force the use of ES6-style imports instead of /// imports. 16 | "no-reference": true, 17 | 18 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 19 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 20 | "no-unnecessary-type-assertion": true, 21 | 22 | // Disallow nonsensical label usage. 23 | "label-position": true, 24 | 25 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 26 | "no-conditional-assignment": true, 27 | 28 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 29 | "no-construct": true, 30 | 31 | // Do not allow super() to be called twice in a constructor. 32 | "no-duplicate-super": true, 33 | 34 | // Do not allow the same case to appear more than once in a switch block. 35 | "no-duplicate-switch-case": true, 36 | 37 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 38 | // rule. 39 | "no-duplicate-variable": [true, "check-parameters"], 40 | 41 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 42 | // instead use a separate variable name. 43 | "no-shadowed-variable": true, 44 | 45 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 46 | "no-empty": [true, "allow-empty-catch"], 47 | 48 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 49 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 50 | "no-floating-promises": true, 51 | 52 | // The 'this' keyword can only be used inside of classes. 53 | "no-invalid-this": true, 54 | 55 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 56 | "no-string-throw": true, 57 | 58 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 59 | "no-unsafe-finally": true, 60 | 61 | 62 | // Disallow duplicate imports in the same file. 63 | "no-duplicate-imports": true, 64 | 65 | 66 | // -- Strong Warnings -- 67 | // These rules should almost never be needed, but may be included due to legacy code. 68 | // They are left as a warning to avoid frustration with blocked deploys when the developer 69 | // understand the warning and wants to deploy anyway. 70 | 71 | // Warn when an empty interface is defined. These are generally not useful. 72 | "no-empty-interface": true, 73 | 74 | // Warn when an import will have side effects. 75 | "no-import-side-effect": true, 76 | 77 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 78 | // most values and let for values that will change. 79 | "no-var-keyword": true, 80 | 81 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 82 | "triple-equals": true, 83 | 84 | // Warn when using deprecated APIs. 85 | "deprecation": false, 86 | 87 | // -- Light Warnigns -- 88 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 89 | // if TSLint supported such a level. 90 | 91 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 92 | // (Even better: check out utils like .map if transforming an array!) 93 | "prefer-for-of": true, 94 | 95 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 96 | "unified-signatures": true, 97 | 98 | // Prefer const for values that will not change. This better documents code. 99 | "prefer-const": true, 100 | 101 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 102 | "trailing-comma": true, 103 | "arrow-return-shorthand": true, 104 | "callable-types": true, 105 | "class-name": true, 106 | "comment-format": [ 107 | true, 108 | "check-space" 109 | ], 110 | "curly": true, 111 | "forin": true, 112 | "import-blacklist": [ 113 | true, 114 | "rxjs" 115 | ], 116 | "import-spacing": true, 117 | "indent": [ 118 | true, 119 | "spaces" 120 | ], 121 | "interface-over-type-literal": true, 122 | "member-access": false, 123 | "no-arg": true, 124 | "no-bitwise": true, 125 | "no-console": [ 126 | true, 127 | "debug", 128 | "time", 129 | "timeEnd", 130 | "trace" 131 | ], 132 | "no-debugger": true, 133 | "no-eval": true, 134 | "no-inferrable-types": [ 135 | true, 136 | "ignore-params" 137 | ], 138 | "no-misused-new": true, 139 | "no-non-null-assertion": true, 140 | "no-string-literal": false, 141 | "no-switch-case-fall-through": true, 142 | "no-unnecessary-initializer": true, 143 | "object-literal-sort-keys": false, 144 | "one-line": [ 145 | true, 146 | "check-open-brace", 147 | "check-catch", 148 | "check-else", 149 | "check-whitespace" 150 | ], 151 | "radix": true, 152 | "semicolon": [ 153 | true, 154 | "always" 155 | ], 156 | "typedef-whitespace": [ 157 | true, 158 | { 159 | "call-signature": "nospace", 160 | "index-signature": "nospace", 161 | "parameter": "nospace", 162 | "property-declaration": "nospace", 163 | "variable-declaration": "nospace" 164 | } 165 | ], 166 | "variable-name": false 167 | }, 168 | 169 | "defaultSeverity": "error" 170 | } 171 | --------------------------------------------------------------------------------