mirror of
https://github.com/prymitive/karma
synced 2026-08-17 11:27:01 +00:00
feat(ci): report webpack bundle size diffs on PRs
This commit is contained in:
committed by
Łukasz Mierzwa
parent
5dd1251010
commit
d49ea20530
+23
@@ -62,6 +62,18 @@ jobs:
|
||||
after_success:
|
||||
- travis_retry curl -s --connect-timeout 30 --fail https://codecov.io/bash | bash -s -- -F ui
|
||||
|
||||
- stage: Test
|
||||
name: Webpack bundle size diff
|
||||
if: (repo = prymitive/karma AND type = pull_request)
|
||||
<<: *DEFAULTS_JS
|
||||
env:
|
||||
# GITHUB_TOKEN for CI Bundle size diff comments and gist uploads
|
||||
- secure: "Or5fiXZfgIsXvzoOdEprRwJ0uwUjMvxGHE3LG+h3OsIBO6WA8vgOqVOjqVHV2dgC9cSjm5A7MX52S7cDqfkQDkgnVHpVxRwDC9n9O4vnaFdCZ4nC+d18z8dikbiwgdeWQ+Wi6RhZENye1Lu5sBaAJ09wYgx9lNdEVpRaTUvUw6grSlESJZSXoxfxWWpmTyx+yPH4sxuWjZ7gCspDX9s9k4fjpY4LkhQQwLlk8wPc2hfDg48e+K1OR6sYB8uRS33Xc4fQtzElzazmaZ0fn77h5ysDgC1g/ko+E2j8HHMbZvFpzYpm1bCpIv1G/0A2ItH7gT3HsuwkDvfH/it56JTCbBWJJ+hTDeswCQNu0h797QM6jv0o5wgKpHR1t+AeM9vDe4Ds0pAXouJz0LJewNOdNvi5O1BZA9OooKc34hwTJs/zj5NwiZuOyPSMhDBGa++Vhsr9K3rPD9+97M2hac6NO6TBVWZjvqJilmkjJs+bKrl//ClBvdhDGkJNDbB+2emdD1/wzpPVJPp3IRhzeEF89IVE58qE+OQnIwtbEZ2W1ct6Ep7ZJdrXWc/VBdJB1ELfUtNmkvWFZD5IJfnb/Z5MS6iespXlV5alPQ7eZ2jNl3tn7uDaCStuQN1tO2wthNnsSU/OkfFRch/Ks3gYC5+v7n8aJMkTYFmHr4Y/xlXBsrA="
|
||||
script:
|
||||
- ./scripts/gist-get.py 6a1f1aa15a7a308dac7ea2726021836c ui
|
||||
- make -C ui build/stats.json
|
||||
- ./scripts/cra-bundle-stats-diff.py ui/stats.json ui/build/stats.json
|
||||
|
||||
- stage: Lint
|
||||
name: Lint git commit
|
||||
if: (repo = prymitive/karma AND type != pull_request) OR (fork = true AND type = pull_request)
|
||||
@@ -273,6 +285,17 @@ jobs:
|
||||
- travis_retry curl -s --connect-timeout 30 --fail https://cli-assets.heroku.com/install.sh | sh
|
||||
- travis_retry /usr/local/bin/heroku container:release web --app karma-demo
|
||||
|
||||
- stage: Build and Deploy
|
||||
name: Webpack bundle size snapshot
|
||||
if: repo = prymitive/karma AND type = push AND branch = master
|
||||
<<: *DEFAULTS_JS
|
||||
env:
|
||||
# GITHUB_TOKEN for CI Bundle size diff comments and gist uploads
|
||||
- secure: "Or5fiXZfgIsXvzoOdEprRwJ0uwUjMvxGHE3LG+h3OsIBO6WA8vgOqVOjqVHV2dgC9cSjm5A7MX52S7cDqfkQDkgnVHpVxRwDC9n9O4vnaFdCZ4nC+d18z8dikbiwgdeWQ+Wi6RhZENye1Lu5sBaAJ09wYgx9lNdEVpRaTUvUw6grSlESJZSXoxfxWWpmTyx+yPH4sxuWjZ7gCspDX9s9k4fjpY4LkhQQwLlk8wPc2hfDg48e+K1OR6sYB8uRS33Xc4fQtzElzazmaZ0fn77h5ysDgC1g/ko+E2j8HHMbZvFpzYpm1bCpIv1G/0A2ItH7gT3HsuwkDvfH/it56JTCbBWJJ+hTDeswCQNu0h797QM6jv0o5wgKpHR1t+AeM9vDe4Ds0pAXouJz0LJewNOdNvi5O1BZA9OooKc34hwTJs/zj5NwiZuOyPSMhDBGa++Vhsr9K3rPD9+97M2hac6NO6TBVWZjvqJilmkjJs+bKrl//ClBvdhDGkJNDbB+2emdD1/wzpPVJPp3IRhzeEF89IVE58qE+OQnIwtbEZ2W1ct6Ep7ZJdrXWc/VBdJB1ELfUtNmkvWFZD5IJfnb/Z5MS6iespXlV5alPQ7eZ2jNl3tn7uDaCStuQN1tO2wthNnsSU/OkfFRch/Ks3gYC5+v7n8aJMkTYFmHr4Y/xlXBsrA="
|
||||
script:
|
||||
- make -C ui build/stats.json
|
||||
- ./scripts/gist-upload.py 6a1f1aa15a7a308dac7ea2726021836c ui/build/stats.json
|
||||
|
||||
- stage: E2E
|
||||
name: Test demo site
|
||||
if: (repo = prymitive/karma AND type != pull_request) OR (fork = true AND type = pull_request)
|
||||
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
|
||||
Bundle = namedtuple('Bundle' , 'seq bundleName totalBytes files')
|
||||
|
||||
Diff = namedtuple('Diff', 'status path oldBytes newBytes isBigger diff')
|
||||
BundleDiff = namedtuple('BundleDiff', 'total files')
|
||||
|
||||
|
||||
class Status:
|
||||
added = 'added'
|
||||
modified = 'modified'
|
||||
deleted = 'deleted'
|
||||
|
||||
|
||||
def humanize(value):
|
||||
abbrevs = (
|
||||
(1<<20, 'MB'),
|
||||
(1<<10, 'kB'),
|
||||
(1, 'bytes')
|
||||
)
|
||||
if value == 1:
|
||||
return '1 byte'
|
||||
for factor, suffix in abbrevs:
|
||||
if value >= factor:
|
||||
break
|
||||
return '%d %s' % (value / factor, suffix)
|
||||
|
||||
|
||||
def readBundle(path):
|
||||
bundles = []
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for result in data['results']:
|
||||
seq = os.path.basename(result['bundleName']).split('.')[0]
|
||||
bundle = Bundle(
|
||||
seq=seq,
|
||||
bundleName=result['bundleName'],
|
||||
totalBytes=result['totalBytes'],
|
||||
files=result['files'])
|
||||
bundles.append(bundle)
|
||||
return bundles
|
||||
|
||||
|
||||
def printRow(diff, element):
|
||||
if diff.status == Status.added:
|
||||
status = '+'
|
||||
elif diff.status == Status.modified:
|
||||
status = 'M'
|
||||
else:
|
||||
status = '-'
|
||||
|
||||
sign = '+' if diff.isBigger else ''
|
||||
ob = humanize(diff.oldBytes) if diff.oldBytes > 0 else ''
|
||||
nb = humanize(diff.newBytes) if diff.newBytes > 0 else ''
|
||||
|
||||
return '''<tr>
|
||||
<{element}>{status}</{element}>
|
||||
<{element}>{path}</{element}>
|
||||
<{element}>{ob}</{element}>
|
||||
<{element}>{nb}</{element}>
|
||||
<{element}>{sign}{diff}</{element}>
|
||||
</tr>'''.format(
|
||||
element=element,
|
||||
status=status, path=diff.path,
|
||||
ob=ob, nb=nb,
|
||||
sign=sign, diff=humanize(diff.diff))
|
||||
|
||||
|
||||
def printBundleTable(diffs, element):
|
||||
rows = []
|
||||
for diff in diffs:
|
||||
rows.append(printRow(diff, element))
|
||||
return '<table><tbody>{body}</tbody></table>'.format(body='\n'.join(rows))
|
||||
|
||||
|
||||
def printBundleDiff(bundleDiff):
|
||||
summary = printBundleTable([bundleDiff.total], 'th')
|
||||
details = printBundleTable(bundleDiff.files, 'td')
|
||||
|
||||
return '''
|
||||
<details>
|
||||
<summary>{summary}</summary>
|
||||
{details}
|
||||
</details>
|
||||
'''.format(summary=summary, details=details)
|
||||
|
||||
|
||||
def makeDiff(path, oldBytes, newBytes):
|
||||
if oldBytes > 0 and newBytes > 0:
|
||||
status = Status.modified
|
||||
elif oldBytes == 0:
|
||||
status = Status.added
|
||||
else:
|
||||
status = Status.deleted
|
||||
return Diff(
|
||||
status=status,
|
||||
path=path,
|
||||
oldBytes=oldBytes,
|
||||
newBytes=newBytes,
|
||||
isBigger=newBytes > oldBytes,
|
||||
diff=newBytes - oldBytes,
|
||||
)
|
||||
|
||||
|
||||
def diffBundle(ba, bb):
|
||||
filesDiffs = []
|
||||
if ba.totalBytes != bb.totalBytes:
|
||||
totalDiff = makeDiff(ba.bundleName, ba.totalBytes, bb.totalBytes)
|
||||
|
||||
for aFile in ba.files:
|
||||
aSize = ba.files[aFile]['size']
|
||||
if aFile in bb.files:
|
||||
bSize = bb.files[aFile]['size']
|
||||
if aSize != bSize:
|
||||
filesDiffs.append(makeDiff(aFile, aSize, bSize))
|
||||
else:
|
||||
filesDiffs.append(makeDiff(aFile, aSize, 0))
|
||||
|
||||
for bFile in bb.files:
|
||||
bSize = bb.files[bFile]['size']
|
||||
if bFile not in ba.files:
|
||||
filesDiffs.append(makeDiff(bFile, 0, bSize))
|
||||
|
||||
return BundleDiff(total=totalDiff, files=filesDiffs)
|
||||
|
||||
|
||||
def summarize(diffs):
|
||||
totalDiff = 0
|
||||
for diff in diffs:
|
||||
totalDiff += diff.total.diff
|
||||
sign = '+' if totalDiff > 0 else ''
|
||||
return '### Total diff: {sign}{totalDiff}'.format(
|
||||
sign=sign, totalDiff=humanize(totalDiff))
|
||||
|
||||
|
||||
def postComment(diffs, pr, token, owner='prymitive', repo='karma'):
|
||||
api = 'api.github.com'
|
||||
uri = 'https://{api}/repos/{owner}/{repo}/issues/{pr}/comments'.format(
|
||||
api=api, owner=owner, repo=repo, pr=pr)
|
||||
|
||||
rows = []
|
||||
for diff in diffs:
|
||||
rows.append(printBundleDiff(diff))
|
||||
summary = summarize(diffs)
|
||||
data = '*Webpack bundle size diff*\n{summary}\n{rows}'.format(
|
||||
summary=summary, rows='\n'.join(rows))
|
||||
encoded = json.dumps({'body': data})
|
||||
|
||||
req = urllib2.Request(uri)
|
||||
req.add_header('Authorization', 'token %s' % token)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
response = urllib2.urlopen(req, encoded)
|
||||
except Exception as e:
|
||||
print("Request to '%s' failed: %s" % (uri, e))
|
||||
|
||||
|
||||
def diffBundles(a, b):
|
||||
diffs = []
|
||||
for ba in a:
|
||||
found = False
|
||||
for bb in b:
|
||||
if ba.seq == bb.seq:
|
||||
found = True
|
||||
d = diffBundle(ba, bb)
|
||||
if d:
|
||||
diffs.append(d)
|
||||
break
|
||||
if not found:
|
||||
bb = Bundle(
|
||||
seq=ba.seq,
|
||||
bundleName=ba.bundleName,
|
||||
totalBytes=0,
|
||||
files={}
|
||||
)
|
||||
d = diffBundle(ba, bb)
|
||||
if d:
|
||||
diffs.append(d)
|
||||
|
||||
for bb in b:
|
||||
found = False
|
||||
for ba in a:
|
||||
if bb.seq == ba.seq:
|
||||
found = True
|
||||
if not found:
|
||||
ba = Bundle(
|
||||
seq=bb.seq,
|
||||
bundleName=bb.bundleName,
|
||||
totalBytes=0,
|
||||
files={}
|
||||
)
|
||||
d = diffBundle(ba, bb)
|
||||
if d:
|
||||
diffs.append(d)
|
||||
|
||||
diffs.sort(key=lambda x: x.total.newBytes, reverse=True)
|
||||
return diffs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: PATH1 PATH2')
|
||||
sys.exit(1)
|
||||
|
||||
token = os.getenv('GITHUB_TOKEN')
|
||||
pr = os.getenv('TRAVIS_PULL_REQUEST')
|
||||
bundleA = readBundle(sys.argv[1])
|
||||
bundleB = readBundle(sys.argv[2])
|
||||
|
||||
if not token:
|
||||
print('GITHUB_TOKEN env variable is missing')
|
||||
sys.exit(1)
|
||||
if not pr:
|
||||
print('TRAVIS_PULL_REQUEST env variable is missing')
|
||||
sys.exit(1)
|
||||
if not bundleA or not bundleB:
|
||||
print('Usage: PATH1 PATH2')
|
||||
sys.exit(1)
|
||||
|
||||
diffs = diffBundles(bundleA, bundleB)
|
||||
if diffs:
|
||||
print('Found diffs, posting to GitHub')
|
||||
postComment(diffs, pr, token)
|
||||
else:
|
||||
print('No diff found')
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
|
||||
def downloadFile(workdir, path, uri, token):
|
||||
req = urllib2.Request(uri)
|
||||
req.add_header('Authorization', 'token %s' % token)
|
||||
try:
|
||||
response = urllib2.urlopen(req)
|
||||
except Exception as e:
|
||||
print("Request to '%s' failed: %s" % (uri, e))
|
||||
else:
|
||||
with open(os.path.join(workdir, path), "wb") as local_file:
|
||||
local_file.write(response.read())
|
||||
|
||||
|
||||
def getGist(workdir, gist_id, token):
|
||||
api = 'api.github.com'
|
||||
uri = 'https://{api}/gists/{gist_id}'.format(api=api, gist_id=gist_id)
|
||||
|
||||
req = urllib2.Request(uri)
|
||||
req.add_header('Authorization', 'token %s' % token)
|
||||
try:
|
||||
response = urllib2.urlopen(req)
|
||||
except Exception as e:
|
||||
print("Request to '%s' failed: %s" % (uri, e))
|
||||
else:
|
||||
data = json.load(response)
|
||||
for filename, meta in data['files'].items():
|
||||
print('Fetching {filename} from {uri}'.format(
|
||||
filename=filename, uri=meta['raw_url']))
|
||||
downloadFile(workdir, filename, meta['raw_url'], token)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: GIST_ID WORKDIR')
|
||||
sys.exit(1)
|
||||
|
||||
token = os.getenv('GITHUB_TOKEN')
|
||||
if not token:
|
||||
print('GITHUB_TOKEN env variable is missing')
|
||||
sys.exit(1)
|
||||
|
||||
gist_id = sys.argv[1]
|
||||
workdir = sys.argv[2]
|
||||
if not gist_id or not workdir:
|
||||
print('Usage: GIST_ID WORKDIR')
|
||||
sys.exit(1)
|
||||
|
||||
getGist(workdir, gist_id, token)
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
|
||||
def patchGist(filepath, gist_id, token):
|
||||
api = 'api.github.com'
|
||||
uri = 'https://{api}/gists/{gist_id}'.format(api=api, gist_id=gist_id)
|
||||
|
||||
with open(filepath) as f:
|
||||
data = f.read()
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
payload = {
|
||||
"description": "Webpack bundle size stats",
|
||||
"files": {
|
||||
filename: {
|
||||
"content": data,
|
||||
"filename": filename
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req = urllib2.Request(uri)
|
||||
req.get_method = lambda: 'PATCH'
|
||||
req.add_header('Authorization', 'token %s' % token)
|
||||
try:
|
||||
response = urllib2.urlopen(req, json.dumps(payload))
|
||||
except Exception as e:
|
||||
print("Request to '%s' failed: %s" % (uri, e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: GIST_ID FILE')
|
||||
sys.exit(1)
|
||||
|
||||
token = os.getenv('GITHUB_TOKEN')
|
||||
if not token:
|
||||
print('GITHUB_TOKEN env variable is missing')
|
||||
sys.exit(1)
|
||||
|
||||
gist_id = sys.argv[1]
|
||||
filepath = sys.argv[2]
|
||||
if not gist_id or not filepath:
|
||||
print('Usage: GIST_ID FILE')
|
||||
sys.exit(1)
|
||||
|
||||
patchGist(filepath, gist_id, token)
|
||||
@@ -40,3 +40,6 @@ lint-docs: node_modules/markdownlint-cli/markdownlint.js
|
||||
.PHONY: format
|
||||
format: node_modules/prettier/bin-prettier.js
|
||||
node_modules/prettier/bin-prettier.js --write 'src/**/*.js' 'src/**/*.tsx'
|
||||
|
||||
build/stats.json: build
|
||||
node_modules/source-map-explorer/dist/cli.js build/static/*/*.{js,css} --json > build/stats.json
|
||||
|
||||
Reference in New Issue
Block a user