Skip to content

Commit 629cf64

Browse files
committed
Add CI workflow to compute diff between files dist files
1 parent d394049 commit 629cf64

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Generate a markdown table with the difference in size of the dist files between the base and the PR.
3+
*/
4+
5+
/*
6+
Usage:
7+
```shell
8+
BASE_DIST_FILES='{"src/Autocomplete/assets/dist/controller.js":{"size":15382,"size_gz":3716,"size_brotli":3125},"src/Chartjs/assets/dist/controller.js":{"size":2281,"size_gz":771,"size_brotli":642},"src/Cropperjs/assets/dist/controller.js":{"size":1044,"size_gz":475,"size_brotli":371}}' \
9+
PR_DIST_FILES='{"src/Chartjs/assets/dist/controller.js":{"size":1281,"size_gz":171,"size_brotli":641},"src/Cropperjs/assets/dist/controller.js":{"size":1044,"size_gz":475,"size_brotli":371},"src/Cropperjs/assets/dist/style.min.css":{"size":32,"size_gz":66,"size_brotli":34},"src/Dropzone/assets/dist/controller.js":{"size":3199,"size_gz":816,"size_brotli":634},"src/Map/src/Bridge/Google/assets/dist/foo.js":{"size":3199,"size_gz":816,"size_brotli":634}}' \
10+
GITHUB_REPOSITORY='symfony/ux' \
11+
GITHUB_HEAD_REF='my-branch-name' \
12+
node .github/generate-dist-files-size-diff.mjs
13+
```
14+
*/
15+
16+
if (!process.env.BASE_DIST_FILES) {
17+
throw new Error('Missing or invalid "BASE_DIST_FILES" env variable.');
18+
}
19+
20+
if (!process.env.PR_DIST_FILES) {
21+
throw new Error('Missing or invalid "PR_DIST_FILES" env variable.');
22+
}
23+
24+
if (!process.env.GITHUB_REPOSITORY) {
25+
throw new Error('Missing or invalid "GITHUB_REPOSITORY" env variable.');
26+
}
27+
28+
if (!process.env.GITHUB_HEAD_REF) {
29+
throw new Error('Missing or invalid "GITHUB_HEAD_REF" env variable.');
30+
}
31+
32+
/**
33+
* Adapted from https://gist.github.com/zentala/1e6f72438796d74531803cc3833c039c?permalink_comment_id=4455218#gistcomment-4455218
34+
* @param {number} bytes
35+
* @param {number} digits
36+
* @returns {string}
37+
*/
38+
function formatBytes(bytes, digits = 2) {
39+
if (bytes === 0) {
40+
return '0 B';
41+
}
42+
const sizes = [`B`, 'kB', 'MB'];
43+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
44+
45+
return parseFloat((bytes / Math.pow(1024, i)).toFixed(digits)) + ' ' + sizes[i];
46+
}
47+
48+
/**
49+
* @param {number} from
50+
* @param {number} to
51+
* @returns {number}
52+
*/
53+
function computeDiffPercent(from, to) {
54+
if (from === to) {
55+
return 0;
56+
}
57+
58+
return Number(((from - to) / to * -100).toFixed(1));
59+
}
60+
61+
/**
62+
* @param {number} percent
63+
* @returns {string}
64+
*/
65+
function formatDiffPercent(percent) {
66+
return percent > 0 ? `+${percent}% 📈` : percent < 0 ? `${percent}% 📉` : `${percent}%`;
67+
}
68+
69+
export function main() {
70+
const repoUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}`;
71+
/** @type {Record<string, {size: number, size_gz: number, size_brotli: number}>} */
72+
const base = JSON.parse(process.env.BASE_DIST_FILES);
73+
/** @type {Record<string, {size: number, size_gz: number, size_brotli: number}>} */
74+
const pr = JSON.parse(process.env.PR_DIST_FILES);
75+
let output = '<h1>📊 Dist packagesFiles size difference</h1>\n\n';
76+
77+
/**
78+
* @type {Map<string, {
79+
* meta: {
80+
* packageName: string,
81+
* bridgeName: string,
82+
* url: string,
83+
* },
84+
* files: Set<{
85+
* state: 'added' | 'removed' | 'changed',
86+
* before: {size: number, sizeGz: number, sizeBrotli: number},
87+
* after: {size: number, sizeGz: number, sizeBrotli: number},
88+
* diffPercent: {size: number, sizeGz: number, sizeBrotli: number},
89+
* meta: {fileNameShort: string, fileNameUrl: string}
90+
* }>
91+
* }>}
92+
*/
93+
const packagesFiles = [...new Set([...Object.keys(pr), ...Object.keys(base)])]
94+
.sort()
95+
.reduce((acc, file) => {
96+
const beforeSize = base[file]?.size || 0;
97+
const afterSize = pr[file]?.size || 0;
98+
const beforeSizeGz = base[file]?.size_gz || 0;
99+
const afterSizeGz = pr[file]?.size_gz || 0;
100+
const beforeSizeBrotli = base[file]?.size_brotli || 0;
101+
const afterSizeBrotli = pr[file]?.size_brotli || 0;
102+
103+
if (beforeSize !== afterSize) {
104+
const isBridge = file.includes('src/Bridge'); // we assume that's enough for now
105+
const packageName = file.split('/')[1];
106+
const bridgeName = isBridge ? file.split('/')[4] : '';
107+
const key = isBridge ? `${packageName} (Bridge ${bridgeName})` : packageName;
108+
if (!acc.has(key)) {
109+
acc.set(key, {
110+
meta: {
111+
packageName,
112+
bridgeName,
113+
url: isBridge ? `${repoUrl}/tree/${process.env.GITHUB_HEAD_REF}/src/${packageName}/src/Bridge/${bridgeName}/assets/dist` : `${repoUrl}/tree/${process.env.GITHUB_HEAD_REF}/src/${packageName}/assets/dist`,
114+
}, files: new Set(),
115+
});
116+
}
117+
118+
const added = !base[file] && pr[file];
119+
const removed = base[file] && !pr[file];
120+
121+
acc.get(key).files.add({
122+
state: added ? 'added' : (removed ? 'removed' : 'changed'),
123+
before: { size: beforeSize, sizeGz: beforeSizeGz, sizeBrotli: beforeSizeBrotli },
124+
after: { size: afterSize, sizeGz: afterSizeGz, sizeBrotli: afterSizeBrotli },
125+
diffPercent: {
126+
size: removed ? -100 : (added ? 100 : (computeDiffPercent(beforeSize, afterSize))),
127+
sizeGz: removed ? -100 : (added ? 100 : (computeDiffPercent(beforeSizeGz, afterSizeGz))),
128+
sizeBrotli: removed ? -100 : (added ? 100 : (computeDiffPercent(beforeSizeBrotli, afterSizeBrotli))),
129+
},
130+
meta: {
131+
fileNameShort: file.replace(isBridge ? `src/${file.split('/')[1]}/src/Bridge/${file.split('/')[4]}/assets/dist/` : `src/${file.split('/')[1]}/assets/dist/`, ''),
132+
fileNameUrl: `${repoUrl}/blob/${process.env.GITHUB_HEAD_REF}/${file}`,
133+
},
134+
});
135+
}
136+
137+
return acc;
138+
}, new Map);
139+
140+
if (packagesFiles.size === 0) {
141+
output += 'ℹ️ No difference in dist packagesFiles.\n';
142+
return output;
143+
}
144+
145+
output += 'Thanks for the PR! Here is the difference in size of the dist packagesFiles between the base and the PR.\n';
146+
output += 'Please review the changes and make sure they are expected.\n\n';
147+
output += `<table>
148+
<thead><tr><th>File</th><th>Before (Size / Gzip / Brotli)</th><th>After (Size / Gzip / Brotli)</th></tr></thead>
149+
<tbody>`;
150+
for (const [pkgKey, pkg] of packagesFiles.entries()) {
151+
output += `<tr><td colspan="3"><a href="${pkg.meta.url}"><b>${pkgKey}</b></a></td></tr>`;
152+
for (const file of pkg.files) {
153+
output += `<tr>
154+
<td><a href="${file.meta.fileNameUrl}"><code>${file.meta.fileNameShort}</code></a></td>
155+
`;
156+
output += file.state === 'added'
157+
? `<td><em>Added</em></td>`
158+
: `<td>
159+
<code>${formatBytes(file.before.size)}</code>
160+
/ <code>${formatBytes(file.before.sizeGz)}</code>
161+
/ <code>${formatBytes(file.before.sizeBrotli)}</code>
162+
</td>`;
163+
output += file.state === 'removed'
164+
? `<td><em>Removed</em></td>`
165+
: `<td>
166+
<code>${formatBytes(file.after.size)}</code>${file.state === 'changed' ? `: ${formatDiffPercent(file.diffPercent.size)}<br/>` : ' / '}
167+
<code>${formatBytes(file.after.sizeGz)}</code>${file.state === 'changed' ? `: ${formatDiffPercent(file.diffPercent.sizeGz)}<br/>` : ' / '}
168+
<code>${formatBytes(file.after.sizeBrotli)}</code>${file.state === 'changed' ? `: ${formatDiffPercent(file.diffPercent.sizeBrotli)}` : ''}
169+
</td>`;
170+
output += `</tr>`;
171+
}
172+
}
173+
output += `</tbody>
174+
</table>
175+
`;
176+
177+
return output;
178+
}
179+
180+
if (!process.env.CI) {
181+
console.log(main());
182+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Dist Files Size Diff
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'src/*/assets/dist/**'
7+
- 'src/*/src/Bridge/*/assets/dist/**'
8+
9+
jobs:
10+
dist-files-size-diff:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
pull-requests: write # for marocchino/sticky-pull-request-comment@v2
14+
steps:
15+
- name: Configure git
16+
run: |
17+
git config --global user.email ""
18+
git config --global user.name "github-action[bot]"
19+
20+
- uses: marocchino/sticky-pull-request-comment@v2
21+
with:
22+
message: |
23+
⏳ The dist files size difference is being calculated...
24+
25+
- uses: actions/checkout@v4
26+
with:
27+
ref: ${{ github.base_ref }}
28+
29+
- name: Get dist files size (from base branch)
30+
id: base-dist-files
31+
run: |
32+
set -e
33+
34+
FILES=$(find src -mindepth 2 -path '*/assets/dist/*' \( -name "*.js" -o -name "*.css" \) -not \( -path '*/tests/*' -o -path '*/public/*' -o -path '*/vendor/*' \) | sort | while read -r file; do
35+
echo "{\"$file\": {\"size\": $(wc -c < "$file"), \"size_gz\": $(gzip -c "$file" | wc -c), \"size_brotli\": $(brotli -c "$file" | wc -c)}}"
36+
done | jq -s 'add' -c)
37+
38+
echo "files=$FILES" >> $GITHUB_OUTPUT
39+
40+
- uses: actions/checkout@v4
41+
42+
- name: Get dist files size (from pull request)
43+
id: pr-dist-files
44+
run: |
45+
set -e
46+
47+
FILES=$(find src -mindepth 2 -path '*/assets/dist/*' \( -name "*.js" -o -name "*.css" \) -not \( -path '*/tests/*' -o -path '*/public/*' -o -path '*/vendor/*' \) | sort | while read -r file; do
48+
echo "{\"$file\": {\"size\": $(wc -c < "$file"), \"size_gz\": $(gzip -c "$file" | wc -c), \"size_brotli\": $(brotli -c "$file" | wc -c)}}"
49+
done | jq -s 'add' -c)
50+
51+
echo "files=$FILES" >> $GITHUB_OUTPUT
52+
53+
- name: Generate the diff
54+
id: diff
55+
uses: actions/github-script@v7
56+
env:
57+
BASE_DIST_FILES: ${{ steps.base-dist-files.outputs.files }}
58+
PR_DIST_FILES: ${{ steps.pr-dist-files.outputs.files }}
59+
with:
60+
result-encoding: string
61+
script: |
62+
console.log(process.env);
63+
console.log(context);
64+
const { main } = await import('${{ github.workspace }}/.github/generate-dist-files-size-diff.mjs')
65+
66+
return await main()
67+
68+
- name: Comment on the pull request (if any failure)
69+
if: ${{ failure() }}
70+
uses: marocchino/sticky-pull-request-comment@v2
71+
with:
72+
message: |
73+
❌ The dist files size difference could not be calculated. Please check the logs for more details.
74+
75+
- name: Comment on the pull request (if success)
76+
if: ${{ always() && steps.diff.conclusion == 'success' }}
77+
uses: marocchino/sticky-pull-request-comment@v2
78+
with:
79+
message: ${{ steps.diff.outputs.result }}

0 commit comments

Comments
 (0)