Skip to content

Commit d8d7976

Browse files
ortagaearon
authored andcommitted
Adds Danger and a rule showing build size differences (#11865)
* Adds danger_js with an initial rule for warning about large PRs Signed-off-by: Anandaroop Roy <roop@artsymail.com> * [WIP] Get the before and after for the build results * [Dev] More work on the Dangerfile * [Danger] Split the reports into sections based on their package * Remove the --extract-errors on the circle build * [Danger] Improve the lookup for previous -> current build to also include the environment * Fix rebase
1 parent 4f309f8 commit d8d7976

File tree

7 files changed

+862
-87
lines changed

7 files changed

+862
-87
lines changed

dangerfile.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const {markdown, danger} = require('danger');
11+
const fetch = require('node-fetch');
12+
13+
const {generateResultsArray} = require('./scripts/rollup/stats');
14+
const {readFileSync} = require('fs');
15+
16+
const currentBuildResults = JSON.parse(
17+
readFileSync('./scripts/rollup/results.json')
18+
);
19+
20+
/**
21+
* Generates a Markdown table
22+
* @param {string[]} headers
23+
* @param {string[][]} body
24+
*/
25+
function generateMDTable(headers, body) {
26+
const tableHeaders = [
27+
headers.join(' | '),
28+
headers.map(() => ' --- ').join(' | '),
29+
];
30+
31+
const tablebody = body.map(r => r.join(' | '));
32+
return tableHeaders.join('\n') + '\n' + tablebody.join('\n');
33+
}
34+
35+
/**
36+
* Generates a user-readable string from a percentage change
37+
* @param {string[]} headers
38+
*/
39+
function emojiPercent(change) {
40+
if (change > 0) {
41+
return `:small_red_triangle:+${change}%`;
42+
} else if (change <= 0) {
43+
return `${change}%`;
44+
}
45+
}
46+
47+
// Grab the results.json before we ran CI via the GH API
48+
// const baseMerge = danger.github.pr.base.sha
49+
const parentOfOldestCommit = danger.git.commits[0].parents[0];
50+
const commitURL = sha =>
51+
`http://react.zpao.com/builds/master/_commits/${sha}/results.json`;
52+
53+
fetch(commitURL(parentOfOldestCommit)).then(async response => {
54+
const previousBuildResults = await response.json();
55+
const results = generateResultsArray(
56+
currentBuildResults,
57+
previousBuildResults
58+
);
59+
60+
const percentToWarrentShowing = 1;
61+
const packagesToShow = results
62+
.filter(
63+
r =>
64+
Math.abs(r.prevFileSizeChange) > percentToWarrentShowing ||
65+
Math.abs(r.prevGzipSizeChange) > percentToWarrentShowing
66+
)
67+
.map(r => r.packageName);
68+
69+
if (packagesToShow.length) {
70+
let allTables = [];
71+
72+
// Highlight React and React DOM changes inline
73+
// e.g. react: `react.production.min.js`: -3%, `react.development.js`: +4%
74+
75+
if (packagesToShow.includes('react')) {
76+
const reactProd = results.find(
77+
r => r.bundleType === 'UMD_PROD' && r.packageName === 'react'
78+
);
79+
if (
80+
reactProd.prevFileSizeChange !== 0 ||
81+
reactProd.prevGzipSizeChange !== 0
82+
) {
83+
const changeSize = emojiPercent(reactProd.prevFileSizeChange);
84+
const changeGzip = emojiPercent(reactProd.prevGzipSizeChange);
85+
markdown(`React: size: ${changeSize}, gzip: ${changeGzip}`);
86+
}
87+
}
88+
89+
if (packagesToShow.includes('react-dom')) {
90+
const reactDOMProd = results.find(
91+
r => r.bundleType === 'UMD_PROD' && r.packageName === 'react-dom'
92+
);
93+
if (
94+
reactDOMProd.prevFileSizeChange !== 0 ||
95+
reactDOMProd.prevGzipSizeChange !== 0
96+
) {
97+
const changeSize = emojiPercent(reactDOMProd.prevFileSizeChange);
98+
const changeGzip = emojiPercent(reactDOMProd.prevGzipSizeChange);
99+
markdown(`ReactDOM: size: ${changeSize}, gzip: ${changeGzip}`);
100+
}
101+
}
102+
103+
// Show a hidden summary table for all diffs
104+
105+
// eslint-disable-next-line no-var
106+
for (var name of new Set(packagesToShow)) {
107+
const thisBundleResults = results.filter(r => r.packageName === name);
108+
const changedFiles = thisBundleResults.filter(
109+
r => r.prevGzipSizeChange !== 0 || r.prevGzipSizeChange !== 0
110+
);
111+
112+
const mdHeaders = [
113+
'File',
114+
'Filesize Diff',
115+
'Gzip Diff',
116+
'Prev Size',
117+
'Current Size',
118+
'Prev Gzip',
119+
'Current Gzip',
120+
'ENV',
121+
];
122+
123+
const mdRows = changedFiles.map(r => [
124+
r.filename,
125+
emojiPercent(r.prevFileSizeChange),
126+
emojiPercent(r.prevGzipSizeChange),
127+
r.prevSize,
128+
r.prevFileSize,
129+
r.prevGzip,
130+
r.prevGzipSize,
131+
r.bundleType,
132+
]);
133+
134+
allTables.push(`\n## ${name}`);
135+
allTables.push(generateMDTable(mdHeaders, mdRows));
136+
}
137+
138+
const summary = `
139+
<details>
140+
<summary>Details of bundled changes.</summary>
141+
142+
<p>Comparing: ${parentOfOldestCommit}...${danger.github.pr.head.sha}</p>
143+
144+
145+
${allTables.join('\n')}
146+
147+
</details>
148+
`;
149+
markdown(summary);
150+
}
151+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"coveralls": "^2.11.6",
4848
"create-react-class": "^15.6.2",
4949
"cross-env": "^5.1.1",
50+
"danger": "^3.0.4",
5051
"del": "^2.0.2",
5152
"derequire": "^2.0.3",
5253
"escape-string-regexp": "^1.0.5",

scripts/circleci/test_entry_point.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
2525
COMMANDS_TO_RUN+=('./scripts/circleci/build.sh')
2626
COMMANDS_TO_RUN+=('yarn test-build --maxWorkers=2')
2727
COMMANDS_TO_RUN+=('yarn test-build-prod --maxWorkers=2')
28+
COMMANDS_TO_RUN+=('node ./scripts/tasks/danger')
2829
COMMANDS_TO_RUN+=('./scripts/circleci/upload_build.sh')
2930
fi
3031

scripts/rollup/results.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,4 @@
400400
"gzip": 525
401401
}
402402
]
403-
}
403+
}

scripts/rollup/stats.js

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,60 +21,86 @@ function saveResults() {
2121
}
2222

2323
function percentChange(prev, current) {
24-
const change = Math.floor((current - prev) / prev * 100);
24+
return Math.floor((current - prev) / prev * 100);
25+
}
2526

27+
function percentChangeString(change) {
2628
if (change > 0) {
2729
return chalk.red.bold(`+${change} %`);
2830
} else if (change <= 0) {
2931
return chalk.green.bold(change + ' %');
3032
}
3133
}
3234

35+
const resultsHeaders = [
36+
'Bundle',
37+
'Prev Size',
38+
'Current Size',
39+
'Diff',
40+
'Prev Gzip',
41+
'Current Gzip',
42+
'Diff',
43+
];
44+
45+
function generateResultsArray(current, prevResults) {
46+
return current.bundleSizes
47+
.map(result => {
48+
const prev = prevResults.bundleSizes.filter(
49+
res =>
50+
res.filename === result.filename &&
51+
res.bundleType === result.bundleType
52+
)[0];
53+
if (result === prev) {
54+
// We didn't rebuild this bundle.
55+
return;
56+
}
57+
58+
const size = result.size;
59+
const gzip = result.gzip;
60+
let prevSize = prev ? prev.size : 0;
61+
let prevGzip = prev ? prev.gzip : 0;
62+
63+
return {
64+
filename: result.filename,
65+
bundleType: result.bundleType,
66+
packageName: result.packageName,
67+
prevSize: filesize(prevSize),
68+
prevFileSize: filesize(size),
69+
prevFileSizeChange: percentChange(prevSize, size),
70+
prevGzip: filesize(prevGzip),
71+
prevGzipSize: filesize(gzip),
72+
prevGzipSizeChange: percentChange(prevGzip, gzip),
73+
};
74+
// Strip any nulls
75+
})
76+
.filter(f => f);
77+
}
78+
3379
function printResults() {
3480
const table = new Table({
35-
head: [
36-
chalk.gray.yellow('Bundle'),
37-
chalk.gray.yellow('Prev Size'),
38-
chalk.gray.yellow('Current Size'),
39-
chalk.gray.yellow('Diff'),
40-
chalk.gray.yellow('Prev Gzip'),
41-
chalk.gray.yellow('Current Gzip'),
42-
chalk.gray.yellow('Diff'),
43-
],
81+
head: resultsHeaders.map(chalk.gray.yellow),
4482
});
45-
currentBuildResults.bundleSizes.forEach(result => {
46-
const matches = prevBuildResults.bundleSizes.filter(
47-
({filename, bundleType}) =>
48-
filename === result.filename && bundleType === result.bundleType
49-
);
50-
if (matches.length > 1) {
51-
throw new Error(`Ambiguous bundle size record for: ${result.filename}`);
52-
}
53-
const prev = matches[0];
54-
if (result === prev) {
55-
// We didn't rebuild this bundle.
56-
return;
57-
}
5883

59-
const size = result.size;
60-
const gzip = result.gzip;
61-
let prevSize = prev ? prev.size : 0;
62-
let prevGzip = prev ? prev.gzip : 0;
84+
const results = generateResultsArray(currentBuildResults, prevBuildResults);
85+
results.forEach(result => {
6386
table.push([
64-
chalk.white.bold(`${result.filename} (${result.bundleType})`),
65-
chalk.gray.bold(filesize(prevSize)),
66-
chalk.white.bold(filesize(size)),
67-
percentChange(prevSize, size),
68-
chalk.gray.bold(filesize(prevGzip)),
69-
chalk.white.bold(filesize(gzip)),
70-
percentChange(prevGzip, gzip),
87+
chalk.white.bold(`${result.filename} (${result.bundleType})`),
88+
chalk.gray.bold(result.prevSize),
89+
chalk.white.bold(result.prevFileSize),
90+
percentChangeString(result.prevFileSizeChange),
91+
chalk.gray.bold(result.prevGzip),
92+
chalk.white.bold(result.prevGzipSize),
93+
percentChangeString(result.prevGzipSizeChange),
7194
]);
7295
});
96+
7397
return table.toString();
7498
}
7599

76100
module.exports = {
101+
currentBuildResults,
102+
generateResultsArray,
77103
printResults,
78104
saveResults,
79-
currentBuildResults,
105+
resultsHeaders,
80106
};

scripts/tasks/danger.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const path = require('path');
11+
const spawn = require('child_process').spawn;
12+
13+
const extension = process.platform === 'win32' ? '.cmd' : '';
14+
15+
// This came from React Native's circle.yml
16+
const token = 'e622517d9f1136ea8900' + '07c6373666312cdfaa69';
17+
spawn(path.join('node_modules', '.bin', 'danger-ci' + extension), [], {
18+
// Allow colors to pass through
19+
stdio: 'inherit',
20+
env: {
21+
...process.env,
22+
DANGER_GITHUB_API_TOKEN: token,
23+
},
24+
}).on('close', function(code) {
25+
if (code !== 0) {
26+
console.error('Danger failed');
27+
} else {
28+
console.log('Danger passed');
29+
}
30+
31+
process.exit(code);
32+
});

0 commit comments

Comments
 (0)