├── .babelrc ├── .env.example ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── docker-compose.yml ├── image ├── demo-image-1.png └── demo-image-2.png ├── package.json └── src ├── TypeDefinition.js ├── app.js ├── bot.js ├── config.js ├── github.js ├── index.js └── replacers.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-flow-strip-types"] 4 | ], 5 | "presets": ["es2015", "stage-0"] 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Access token for the account you want the bot to use 2 | # You can get one accessing your bot account settings 3 | # and going under Developer Settings > Personal access tokens 4 | GITHUB_TOKEN= 5 | 6 | # The webhook secret that GitHub signs the POSTed payloads with. 7 | # This is created when the webhook is defined 8 | GITHUB_WEBHOOK_SECRET= 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-console": 0, 6 | "no-restricted-syntax": 0, 7 | "max-len": [1, 120, 2], 8 | "no-param-reassign": [2, { "props": false }], 9 | "no-continue": 0, 10 | "no-underscore-dangle": 0, 11 | "generator-star-spacing": 0, 12 | "no-duplicate-imports": 0, 13 | "import/no-duplicates": 2, 14 | "no-use-before-define": 0, 15 | "consistent-return": 0, 16 | "spaced-comment": 0, 17 | "prefer-const": [ 2, { "destructuring": "all" }], 18 | "guard-for-in": 0 19 | }, 20 | "plugins": [ 21 | "import" 22 | ], 23 | "env": { 24 | "es6": true, 25 | "node": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | sharedmemory.hash_table_pow=21 9 | esproposal.export_star_as=enable 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea/ 5 | .vscode/ 6 | 7 | lib-cov 8 | *.seed 9 | *.log 10 | *.csv 11 | *.dat 12 | *.out 13 | *.pid 14 | *.gz 15 | *.map 16 | 17 | pids 18 | logs 19 | results 20 | 21 | node_modules 22 | npm-debug.log 23 | 24 | dump.rdb 25 | bundle.js 26 | 27 | dist 28 | coverage 29 | .nyc_output 30 | .env 31 | 32 | yarn.lock 33 | playground.graphql 34 | graphql.config.json 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | build/ 3 | graphql.config.json 4 | playground.graphql 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-onbuild 2 | EXPOSE 7010 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Entria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQLVigilantBot 2 | 3 | ## Installation 4 | First clone this repository, then check `.env.example` for which environment variables 5 | you need to set before running this bot. 6 | We recommend you to create a new GitHub account for your bot, which is going to be 7 | used to author the comments. 8 | 9 | Now run 10 | ```bash 11 | npm install 12 | npm start 13 | ``` 14 | 15 | ### Using Docker 16 | A Dockerfile is also provided, you can use it to run the bot: 17 | 18 | ```bash 19 | docker build -t graphql-vigilant-bot . 20 | docker run --env-file ./.env -p 7010:7010 graphql-vigilant-bot 21 | ``` 22 | Or use `docker-compose`: 23 | ```bash 24 | docker-compose up 25 | ``` 26 | 27 | The bot will be available at http://localhost. 28 | 29 | You can also deploy directly to Heroku: 30 | 31 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 32 | 33 | ## Setup 34 | 35 | You need to add a webhook pointing to this bot, use `application/json` as 36 | the `Content Type`, and select the `Pull request` event. 37 | Make sure to set a secret, and keep note of it, you will need to add it to your `.env` file. 38 | 39 | ## Development 40 | 41 | Since this bot depends on Github webhooks, we gonna need to use [ngrok](https://ngrok.com/download) 42 | to redirect the webhook request to our machine. 43 | 44 | Run: 45 | ```bash 46 | ./ngrok http $PORT 47 | ``` 48 | 49 | Where `$PORT` is the port you are going to run the bot. 50 | 51 | Grab the `*.ngrok.io` URL and add it as webhook on your repo. 52 | 53 | ### How it looks like 54 | 55 | > Syntax Errors 56 | > 57 | > ![demo-1](./image/demo-image-1.png) 58 | 59 | > Breaking Changes 60 | > 61 | > ![demo-2](./image/demo-image-2.png) 62 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GraphQLVigilantBot", 3 | "description": "Github Bot to find breaking changes in your schema.graphql.", 4 | "repository": "https://github.com/entria/graphql-find-breaking-changes-bot", 5 | "logo": "https://avatars1.githubusercontent.com/u/23662721?v=3&s=200", 6 | "keywords": ["node", "koa", "github", "pull requests", "graphql", "entria"], 7 | "env": { 8 | "GITHUB_TOKEN": { 9 | "description": "Access token for the account you want the bot to use. You can get one accessing your bot account settings and going under Developer Settings > Personal access tokens", 10 | "required": true 11 | }, 12 | "GITHUB_WEBHOOK_SECRET": { 13 | "description": "The secret configured while creating the webhook.", 14 | "required": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: . 6 | restart: always 7 | ports: 8 | - "80:7010" 9 | environment: 10 | GITHUB_TOKEN: "${GITHUB_TOKEN}" 11 | GITHUB_WEBHOOK_SECRET: "${GITHUB_WEBHOOK_SECRET}" 12 | -------------------------------------------------------------------------------- /image/demo-image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entria/graphql-vigilant-bot/873ad0430522855acf55ce5fd0b4c4fe72cc1f97/image/demo-image-1.png -------------------------------------------------------------------------------- /image/demo-image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entria/graphql-vigilant-bot/873ad0430522855acf55ce5fd0b4c4fe72cc1f97/image/demo-image-2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@entria/graphql-vigilant-bot", 3 | "version": "0.0.1", 4 | "author": "Jonathan Cardoso Machado ", 5 | "dependencies": { 6 | "babel-cli": "^6.24.1", 7 | "babel-eslint": "^7.2.3", 8 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 9 | "babel-polyfill": "^6.23.0", 10 | "babel-preset-es2015": "^6.24.1", 11 | "babel-preset-stage-0": "^6.24.1", 12 | "buffer-equal-constant-time": "^1.0.1", 13 | "content-type": "^1.0.2", 14 | "dotenv-safe": "^4.0.4", 15 | "github": "^9.2.0", 16 | "graphql": "0.10.3", 17 | "koa": "^2.3.0", 18 | "lodash": "^4.17.4", 19 | "node-fetch": "^1.7.1", 20 | "raw-body": "^2.2.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "3.19", 24 | "eslint-config-airbnb": "^15.0.1", 25 | "eslint-plugin-import": "^2.5.0", 26 | "flow-bin": "^0.48.0", 27 | "nodemon": "^1.11.0", 28 | "pre-commit": "^1.2.2", 29 | "prettier": "^1.4.4" 30 | }, 31 | "license": "MIT", 32 | "lint-staged": { 33 | "*.js": [ 34 | "prettier --write --single-quote true --trailing-comma all --print-width 120", 35 | "git add" 36 | ] 37 | }, 38 | "main": "dist/bot.js", 39 | "pre-commit": "lint:staged", 40 | "repository": "https://github.com/entria/graphql-vigilant-bot", 41 | "scripts": { 42 | "build": "babel src -d dist", 43 | "flow": "flow", 44 | "lint": "eslint src/**", 45 | "lint:staged": "lint-staged", 46 | "prepublish": "npm run build", 47 | "prettier": "prettier --write --single-quote true --trailing-comma all --print-width 120 src/**/*.js", 48 | "start": "npm run build && node dist/index.js", 49 | "start:watch": "nodemon --ignore dist/ src/index.js --exec babel-node", 50 | "watch": "babel -w -d ./dist ./src" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/TypeDefinition.js: -------------------------------------------------------------------------------- 1 | import type { BreakingChange, BreakingChangeType, GraphQLError } from 'graphql'; 2 | 3 | export type GhCommit = { 4 | files: Array, 5 | }; 6 | 7 | export type GhFile = { 8 | filename: string, 9 | status: string, 10 | previous_filename?: string, 11 | }; 12 | 13 | export type GroupedByTypeBreakingChanges = { 14 | [breakingChangeType: BreakingChangeType]: Array, 15 | }; 16 | 17 | export type AnalysisResult = { 18 | file: string, 19 | url: string, 20 | parseError: ?GraphQLError, 21 | breakingChanges: ?GroupedByTypeBreakingChanges, 22 | }; 23 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Koa from 'koa'; 4 | import contentType from 'content-type'; 5 | import getRawBody from 'raw-body'; 6 | import crypto from 'crypto'; 7 | import bufferEquals from 'buffer-equal-constant-time'; 8 | 9 | import { GITHUB_WEBHOOK_SECRET } from './config'; 10 | import Bot from './bot'; 11 | import gh from './github'; 12 | 13 | const signBlob = (key, blob) => { 14 | const signature = crypto.createHmac('sha1', key).update(blob).digest('hex'); 15 | return `sha1=${signature}`; 16 | }; 17 | 18 | const app = new Koa(); 19 | 20 | app.use(async (ctx, next) => { 21 | if (!ctx.request.headers['content-length'] || !ctx.request.headers['content-type']) { 22 | return; 23 | } 24 | 25 | ctx.request.rawBody = await getRawBody(ctx.req, { 26 | length: ctx.request.headers['content-length'], 27 | limit: '5mb', 28 | encoding: contentType.parse(ctx.request).parameters.charset, 29 | }); 30 | 31 | ctx.request.body = JSON.parse(ctx.request.rawBody); 32 | 33 | await next(); 34 | }); 35 | 36 | app.use(async (ctx, next) => { 37 | const signature = ctx.request.headers['x-hub-signature']; 38 | const event = ctx.request.headers['x-github-event']; 39 | const id = ctx.request.headers['x-github-delivery']; 40 | 41 | console.log(`Handling webhook ${id} for event ${event}.`); 42 | const computedSig = new Buffer(signBlob(GITHUB_WEBHOOK_SECRET, ctx.request.rawBody)); 43 | 44 | if (!bufferEquals(new Buffer(signature), computedSig)) { 45 | ctx.throw(500, 'X-Hub-Signature does not match blob signature.'); 46 | } 47 | 48 | ctx.body = 'Ok'; 49 | 50 | if (event === 'pull_request') { 51 | await next(); 52 | } 53 | }); 54 | 55 | app.use(async (ctx) => { 56 | const data = ctx.request.body; 57 | const bot = new Bot(gh); 58 | 59 | await bot.handlePullRequestWebhookPayload(data); 60 | }); 61 | 62 | export default app; 63 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { buildSchema, findBreakingChanges, GraphQLError } from 'graphql'; 4 | import _ from 'lodash'; 5 | 6 | import type { GitHubApi } from 'github'; 7 | import type { BreakingChange } from 'graphql'; 8 | 9 | import replacers from './replacers'; 10 | 11 | import type { GhCommit, GhFile, AnalysisResult } from './TypeDefinition'; 12 | 13 | const GITHUB_PR_OPENED = 'opened'; 14 | const GITHUB_PR_SYNCHRONIZED = 'synchronize'; 15 | const BOT_IDENTIFIER = ''; 16 | const FILE_FILTER = /.*(.graphql|.gql)$/; 17 | 18 | const validActions = [GITHUB_PR_OPENED, GITHUB_PR_SYNCHRONIZED]; 19 | 20 | export default class Bot { 21 | 22 | gh: GitHubApi; 23 | 24 | constructor(gh: GitHubApi) { 25 | this.gh = gh; 26 | } 27 | 28 | static getMessagesForBreakingChanges(breakingChangeType: string, breakingChanges: Array) { 29 | const replacer = replacers[breakingChangeType]; 30 | const messages = [replacer.title]; 31 | 32 | breakingChanges.reduce((arr, item) => { 33 | arr.push(item.description.replace(replacer.from, replacer.to)); 34 | return arr; 35 | }, messages); 36 | 37 | return messages; 38 | } 39 | 40 | static buildSchemaFromEncodedString(content: string) { 41 | const fileContent = Buffer.from(content, 'base64').toString('utf8'); 42 | const schema = buildSchema(fileContent); 43 | return schema; 44 | } 45 | 46 | static filterSchemaFiles(files: Array) { 47 | return files.filter(({ filename }) => filename.match(FILE_FILTER)); 48 | } 49 | 50 | async getLoggedUser() { 51 | return this.gh.users.get({}); 52 | } 53 | 54 | async findThisBotComment( 55 | owner: string, 56 | repo: string, 57 | pullRequestNumber: number, 58 | thisBot: any, 59 | currentPage: ?number = 1, 60 | ) { 61 | const result = await this.gh.issues.getComments({ 62 | owner, 63 | repo, 64 | number: pullRequestNumber, 65 | per_page: 50, 66 | page: currentPage, 67 | }); 68 | 69 | const comments = result.data; 70 | 71 | let found = comments.find(comment => comment.user.id === thisBot.id && comment.body.indexOf(BOT_IDENTIFIER) === 0); 72 | 73 | if (!found && this.gh.hasNextPage(result)) { 74 | found = await this.findThisBotComment(owner, repo, pullRequestNumber, thisBot, currentPage + 1); 75 | } 76 | 77 | return found; 78 | } 79 | 80 | async getFileContent(owner: string, repo: string, path: string, ref: string) { 81 | return this.gh.repos.getContent({ 82 | owner, 83 | repo, 84 | path, 85 | ref, 86 | }); 87 | } 88 | 89 | async getFilesFromCommit(owner: string, repo: string, sha: string) { 90 | const { data }: { data: GhCommit } = await this.gh.repos.getCommit({ 91 | owner, 92 | repo, 93 | sha, 94 | }); 95 | 96 | return data.files; 97 | } 98 | 99 | async getFilesFromPullRequest(owner: string, repo: string, number: string) { 100 | const { data }: { data: Array } = await this.gh.pullRequests.getFiles({ 101 | owner, 102 | repo, 103 | number, 104 | page: 1, 105 | perPage: 300, 106 | }); 107 | 108 | return data; 109 | } 110 | 111 | async updateComment(owner: string, repo: string, id: string, body: string) { 112 | return this.gh.issues.editComment({ 113 | owner, 114 | repo, 115 | id, 116 | body, 117 | }); 118 | } 119 | 120 | async createComment(owner: string, repo: string, pullRequestNumber: string, body: string) { 121 | return this.gh.issues.createComment({ 122 | owner, 123 | repo, 124 | number: pullRequestNumber, 125 | body, 126 | }); 127 | } 128 | 129 | async handlePullRequestWebhookPayload(data: any) { 130 | const { pull_request: pullRequestPayload } = data; 131 | const { repository: repo } = data; 132 | 133 | if (!pullRequestPayload) { 134 | return; 135 | } 136 | 137 | if (!validActions.includes(data.action)) { 138 | return; 139 | } 140 | 141 | console.log('Handling PR:', pullRequestPayload.html_url); 142 | 143 | const { data: thisBot } = await this.getLoggedUser(); 144 | const thisBotComment = await this.findThisBotComment( 145 | repo.owner.login, 146 | repo.name, 147 | pullRequestPayload.number, 148 | thisBot, 149 | ); 150 | 151 | const { base, head } = pullRequestPayload; 152 | 153 | const changedFiles = thisBotComment ? await this.getFilesFromCommit( 154 | head.user.login, 155 | head.repo.name, 156 | head.sha, 157 | ) : await this.getFilesFromPullRequest( 158 | repo.owner.login, 159 | repo.name, 160 | pullRequestPayload.number, 161 | ); 162 | 163 | const changedSchemaFiles = Bot.filterSchemaFiles(changedFiles); 164 | 165 | // No schema files were modified 166 | if (!changedSchemaFiles.length) { 167 | return; 168 | } 169 | 170 | const analysisResults = await changedSchemaFiles.reduce(async (accumP: Promise>, file) => { 171 | const arr = await accumP; 172 | try { 173 | let originalFileName = file.filename; 174 | // verify if the file was renamed, if yes, use previous name 175 | if (file.status === 'renamed') { 176 | // In case there were no changes, ignore this file. 177 | if (file.changes === 0) { 178 | throw new Error('Schema file renamed, but no changes detected.'); 179 | } 180 | originalFileName = file.previous_filename; 181 | } 182 | 183 | const { data: originalFileContent } = await this.getFileContent( 184 | base.user.login, 185 | base.repo.name, 186 | originalFileName, 187 | base.sha, 188 | ); 189 | const { data: changedFileContent } = await this.getFileContent( 190 | head.user.login, 191 | head.repo.name, 192 | file.filename, 193 | head.sha, 194 | ); 195 | let parseError = null; 196 | let breakingChanges = []; 197 | 198 | try { 199 | const originalSchema = Bot.buildSchemaFromEncodedString(originalFileContent.content); 200 | const changedSchema = Bot.buildSchemaFromEncodedString(changedFileContent.content); 201 | 202 | breakingChanges = findBreakingChanges(originalSchema, changedSchema); 203 | } catch (error) { 204 | if (error instanceof GraphQLError) { 205 | parseError = error; 206 | } else { 207 | throw error; 208 | } 209 | } 210 | 211 | arr.push({ 212 | file: file.filename, 213 | url: changedFileContent.html_url, 214 | parseError, 215 | breakingChanges, 216 | }); 217 | } catch (error) { 218 | if (error.code !== 404 && error.message !== 'Schema file renamed, but no changes detected.') { 219 | console.error(error); 220 | } 221 | } 222 | return arr; 223 | }, Promise.resolve([])); 224 | 225 | let commentBody = ['']; 226 | 227 | for (const result of analysisResults) { 228 | commentBody.push(`### File: [\`${result.file}\`](${result.url})`); 229 | 230 | if (!result.breakingChanges.length) { 231 | if (result.parseError) { 232 | const errorMessage = result.parseError.message; 233 | const errorMessagePieces = errorMessage.split('\n\n'); 234 | const message = errorMessagePieces[0]; 235 | const code = errorMessagePieces[1]; 236 | commentBody.push(message); 237 | commentBody.push(`\`\`\`graphql\n${code}\n\`\`\``); 238 | } else { 239 | commentBody.push('No breaking changes detected :tada:'); 240 | } 241 | } 242 | 243 | const breakingChanges = _.groupBy(result.breakingChanges, 'type'); 244 | 245 | for (const breakingChangeType in breakingChanges) { 246 | commentBody = commentBody.concat( 247 | Bot.getMessagesForBreakingChanges(breakingChangeType, breakingChanges[breakingChangeType]), 248 | ); 249 | } 250 | } 251 | 252 | if (thisBotComment) { 253 | if (commentBody.length === 1) { 254 | commentBody.push('No breaking changes detected :tada:'); 255 | } 256 | await this.updateComment(repo.owner.login, repo.name, thisBotComment.id, commentBody.join('\n')); 257 | } else if (commentBody.length > 1) { 258 | await this.createComment(repo.owner.login, repo.name, pullRequestPayload.number, commentBody.join('\n')); 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import path from 'path'; 4 | import dotenvSafe from 'dotenv-safe'; 5 | 6 | const root = path.join.bind(this, __dirname, '../'); 7 | 8 | dotenvSafe.load({ 9 | path: root('.env'), 10 | allowEmptyValues: true, 11 | sample: root('.env.example'), 12 | }); 13 | 14 | export const { 15 | GITHUB_WEBHOOK_SECRET, 16 | PORT, 17 | } = ((process.env: any): { 18 | GITHUB_WEBHOOK_SECRET: string, 19 | PORT: ?string, 20 | [string]: string, 21 | }); 22 | -------------------------------------------------------------------------------- /src/github.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import GitHubApi from 'github'; 4 | import { version } from '../package.json'; 5 | 6 | const gh = new GitHubApi({ 7 | debug: false, 8 | protocol: 'https', 9 | host: 'api.github.com', 10 | headers: { 11 | 'User-Agent': 12 | `GraphQLVigilantBot/${version} (+https://github.com/entria/graphql-vigilant-bot)`, 13 | }, 14 | followRedirects: false, 15 | timeout: 5000, 16 | }); 17 | 18 | gh.authenticate({ 19 | type: 'token', 20 | token: process.env.GITHUB_TOKEN, 21 | }); 22 | 23 | export default gh; 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import 'babel-polyfill'; 4 | import app from './app'; 5 | import { PORT } from './config'; 6 | 7 | const port = PORT || 7010; 8 | 9 | (async () => { 10 | await app.listen(port); 11 | console.log(`GraphQLVigilantBot started on port ${port}`); 12 | })(); 13 | -------------------------------------------------------------------------------- /src/replacers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { BreakingChangeType } from 'graphql'; 4 | 5 | const common = { 6 | wasRemoved: { 7 | //typeName + '.' + fieldName + ' was removed.' 8 | from: /(.*?) was removed/, 9 | to: '`$1` was removed', 10 | }, 11 | wasRemovedFromX: { 12 | // value.name + ' was removed from X type ' + typeName + '.' 13 | from: /(.*?) was removed from (.*?) type (.*)./, 14 | to: '`$1` was removed from `$2` type `$3`.', 15 | }, 16 | }; 17 | 18 | export default { 19 | [BreakingChangeType.FIELD_CHANGED_KIND]: { 20 | //typeName + '.' + fieldName + ' changed type from ' + (oldFieldTypeString + ' to ' + newFieldTypeString + '.'). 21 | title: '#### Field Changed', 22 | from: /(.*?) changed type from (.*?) to (.*)./, 23 | to: '`$1` changed type from `$2` to `$3`.', 24 | }, 25 | [BreakingChangeType.FIELD_REMOVED]: { 26 | title: '#### Field Removed', 27 | from: common.wasRemoved.from, 28 | to: common.wasRemoved.to, 29 | }, 30 | [BreakingChangeType.TYPE_CHANGED_KIND]: { 31 | //typeName + ' changed from ' + (typeKindName(oldType) + ' to ' + typeKindName(newType) + '.') 32 | title: '#### Type Changed Kind', 33 | from: /(.*?) changed from (.*?) to (.*)./, 34 | to: '`$1` changed from `$2` to `$3`.', 35 | }, 36 | [BreakingChangeType.TYPE_REMOVED]: { 37 | title: '#### Type Removed', 38 | from: common.wasRemoved.from, 39 | to: common.wasRemoved.to, 40 | }, 41 | [BreakingChangeType.TYPE_REMOVED_FROM_UNION]: { 42 | title: '#### Type Removed From Union', 43 | from: common.wasRemovedFromX.from, 44 | to: common.wasRemovedFromX.to, 45 | }, 46 | [BreakingChangeType.VALUE_REMOVED_FROM_ENUM]: { 47 | title: '#### Value Removed From Enum', 48 | from: common.wasRemovedFromX.from, 49 | to: common.wasRemovedFromX.to, 50 | }, 51 | [BreakingChangeType.ARG_REMOVED]: { 52 | //oldType.name + '.' + fieldName + ' arg ' + (oldArgDef.name + ' was removed') 53 | title: '#### Arg Removed', 54 | from: /(.*?) arg (.*?) was removed/, 55 | to: '`$1` arg `$2` was removed', 56 | }, 57 | [BreakingChangeType.ARG_CHANGED_KIND]: { 58 | //oldType.name + '.' + fieldName + ' arg ' + (oldArgDef.name + ' has changed type from ') 59 | // + (oldArgDef.type.toString() + ' to ' + newArgDef.type.toString()) 60 | title: '#### Arg Changed Kind', 61 | from: /(.*?) arg (.*?) has changed type from (.*?) to (.*)./, 62 | to: '`$1` arg `$2` has changed type from `$3` to `$4`.', 63 | }, 64 | [BreakingChangeType.NON_NULL_ARG_ADDED]: { 65 | //'A non-null arg ' + newArgDef.name + ' on ' + (newType.name + '.' + fieldName + ' was added') 66 | title: '#### Non-null Arg Added', 67 | from: /A non-null arg (.*?) on (.*?) was added/, 68 | to: 'A non-null arg `$1` on `$2` was added', 69 | }, 70 | [BreakingChangeType.NON_NULL_INPUT_FIELD_ADDED]: { 71 | //'A non-null field ' + fieldName + ' on ' + ('input type ' + newType.name + ' was added.' 72 | title: '#### Non-null Input Field Added', 73 | from: /A non-null field (.*?) on input type (.*?) was added/, 74 | to: 'A non-null field `$1` on input type `$2` was added', 75 | }, 76 | [BreakingChangeType.INTERFACE_REMOVED_FROM_OBJECT]: { 77 | //typeName + ' no longer implements interface ' + (oldInterface.name + '.') 78 | title: '#### Interface Removed From Object', 79 | from: /(.*?) no longer implements interface (.*)./, 80 | to: '`$1` no longer implements interface `$2`.', 81 | }, 82 | }; 83 | --------------------------------------------------------------------------------