diff --git a/.flowconfig b/.flowconfig index 2efcdda566553e..2edf60073bd8d5 100644 --- a/.flowconfig +++ b/.flowconfig @@ -8,6 +8,9 @@ ; Ignore the website subdir /website/.* +; Ignore the Dangerfile +/danger/dangerfile.js + ; Ignore "BUCK" generated dirs /\.buckd/ diff --git a/.gitignore b/.gitignore index 8a860f1ed8e299..15a2a9668a1a9f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ local.properties node_modules *.log .nvm +/danger/node_modules/ # OS X .DS_Store diff --git a/circle.yml b/circle.yml index d5f2dead8c9ad9..69807fb49ddbda 100644 --- a/circle.yml +++ b/circle.yml @@ -34,12 +34,15 @@ dependencies: - npm install # for eslint bot - npm install github@0.2.4 + # for website, danger - cd website && npm install + - cd danger && npm install cache_directories: - "ReactAndroid/build/downloads" - "/home/ubuntu/buck" - "website/node_modules" - "node_modules" + - "danger/node_modules" test: pre: @@ -49,7 +52,9 @@ test: - source scripts/circle-ci-android-setup.sh && waitForAVD override: - # eslint bot. This GitHub token grants public_repo access scope. + # Run Danger against PRs. This GitHub token grants public_repo access scope. The associated account has no privileged access to the React Native repo. The token must be split in this manner to avoid revocation by GitHub. + - cd danger && DANGER_GITHUB_API_TOKEN="e622517d9f1136ea8900""07c6373666312cdfaa69" npm run danger + # eslint bot. This GitHub token grants public_repo access scope. The token must be split in this manner to avoid revocation by GitHub. - cat <(echo eslint; npm run lint --silent -- --format=json; echo flow; npm run flow --silent -- check --json) | GITHUB_TOKEN="af6ef0d15709bc91d""06a6217a5a826a226fb57b7" CI_USER=$CIRCLE_PROJECT_USERNAME CI_REPO=$CIRCLE_PROJECT_REPONAME PULL_REQUEST_NUMBER=$CIRCLE_PR_NUMBER node bots/code-analysis-bot.js - npm run lint # JS tests for dependencies installed with npm3 diff --git a/danger/.babelrc b/danger/.babelrc new file mode 100644 index 00000000000000..0967ef424bce67 --- /dev/null +++ b/danger/.babelrc @@ -0,0 +1 @@ +{} diff --git a/danger/README.md b/danger/README.md new file mode 100644 index 00000000000000..154140cb75c74b --- /dev/null +++ b/danger/README.md @@ -0,0 +1,12 @@ +If you'd like to make changes to the Dangerfile, find an existing PR and copy the URL. + +Then run from the React Native root: + +``` +cd danger +npm install +.. +node danger/node_modules/.bin/danger pr https://github.com/facebook/react-native/pull/1 +``` + +And you will get the responses from parsing the Dangerfile. diff --git a/danger/dangerfile.js b/danger/dangerfile.js new file mode 100644 index 00000000000000..24bbb733571b03 --- /dev/null +++ b/danger/dangerfile.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +const fs = require('fs'); +const includes = require('lodash.includes'); + +import { danger, fail, markdown, warn } from 'danger'; + +const isDocsFile = path => includes(path, 'docs/'); +const editsDocs = danger.git.modified_files.filter(isDocsFile).length > 0; +const addsDocs = danger.git.created_files.filter(isDocsFile).length > 0; +if (addsDocs || editsDocs) { + // Note, this does not yet cover edits to the autogenerated docs + // (e.g. comments within JS source files) + markdown(':page_facing_up: Thanks for your contribution to the docs!'); +} + +const isBlogFile = path => includes(path, 'blog/'); + +// Flags new blog posts. Note that mentions will not be parsed as the access token we're using does +// not belong to the Facebook org (on purpose) +const addsBlogPost = danger.git.created_files.filter(isBlogFile).length > 0; +if (addsBlogPost) { + const message = ':memo: Blog post'; + const idea = 'This PR appears to add a new blog post, ' + + 'and may require further review from the React Native team.'; + warn(`${message} - ${idea}`); + markdown(':memo: This PR requires attention from the @facebook/react-native team.'); +} + +// Flags edits to blog posts +const editsBlogPost = danger.git.modified_files.filter(isBlogFile).length > 0; +if (editsBlogPost) { + const message = ':memo: Blog post'; + const idea = 'This PR appears to edit an existing blog post, ' + + 'and may require further review from the React Native team.'; + warn(`${message} - ${idea}`); + markdown('This PR requires attention from the @facebook/react-native team.'); +} + +// Fails if the description is too short. +if (danger.github.pr.body.length < 10) { + fail(':grey_question: This pull request needs a description.'); +} + +// Warns if the PR title contains [WIP] +const isWIP = includes(danger.github.pr.title, '[WIP]'); +if (isWIP) { + const message = ':construction_worker: Work In Progress'; + const idea = 'This PR appears to be a work in progress, and may not be ready to be merged yet.'; + warn(`${message} - ${idea}`); +} + +// Warns if there are changes to package.json, and tags the team. +const packageChanged = includes(danger.git.modified_files, 'package.json'); +if (packageChanged) { + const message = ':lock: package.json'; + const idea = 'Changes were made to package.json. ' + + 'This will require a manual import by a Facebook employee.'; + warn(`${message} - ${idea}`); + markdown('This PR requires attention from the @facebook/react-native team.'); +} + +// Warns if a test plan is missing. +const gettingStartedChanged = includes(danger.git.modified_files, 'docs/GettingStarted.md'); +const includesTestPlan = danger.github.pr.body.toLowerCase().includes('test plan'); + +// Warns if a test plan is missing, when editing the Getting Started guide. This page needs to be +// tested in all its permutations. +if (!includesTestPlan && gettingStartedChanged) { + const message = ':clipboard: Test Plan'; + const idea = 'This PR appears to be missing a Test Plan.'; + warn(`${message} - ${idea}`); +} +// Doc edits rarely require a test plan. We'll trust the reviewer to push back if one is needed. +if (!includesTestPlan && !editsDocs) { + const message = ':clipboard: Test Plan'; + const idea = 'This PR appears to be missing a Test Plan.'; + warn(`${message} - ${idea}`); +} + +// Tags PRs that have been submitted by a core contributor. +const taskforce = fs.readFileSync('../bots/IssueCommands.txt', 'utf8').split('\n')[0].split(':')[1]; +const isSubmittedByTaskforce = includes(taskforce, danger.github.pr.user.login); +if (isSubmittedByTaskforce) { + markdown('This PR has been submitted by a core contributor.'); +} + +// Warns if the bots whitelist file is updated. +const isBotsCommandsFile = path => includes(path, 'bots/IssueCommands.txt'); +if (isBotsCommandsFile) { + const message = ':exclamation: Bots'; + const idea = 'This PR appears to modify the list of people that may issue commands to the ' + + 'GitHub bot.'; + warn(`${message} - ${idea}`); +} diff --git a/danger/package.json b/danger/package.json new file mode 100644 index 00000000000000..4ec6b832b73a51 --- /dev/null +++ b/danger/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "scripts": { + "danger": "node ./node_modules/.bin/danger" + }, + "devDependencies": { + "danger": "^0.21.2", + "lodash.includes": "^4.3.0" + } +}