#!/usr/bin/env python from collections import namedtuple import json import os import sys import urllib2 Bundle = namedtuple('Bundle' , 'seq ext 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(nbytes): suffixes = ['B', 'KB', 'MB'] i = 0 while abs(nbytes) >= 1024 and i < len(suffixes)-1: nbytes /= 1024. i += 1 f = ('%.1f' % nbytes).rstrip('0').rstrip('.') return '%s %s' % (f, suffixes[i]) def normalizePath(path): if path.startswith('node_modules/') or '/node_modules/' in path: mod = path.partition('/node_modules/')[2].split('/') if mod[0].startswith('@'): return '/'.join(mod[0:2]) return mod[0] return path def mergeFiles(allFiles): files = {} for path, meta in allFiles.items(): p = normalizePath(path) if p not in files: files[p] = 0 files[p] += meta['size'] return files def readBundle(path): bundles = [] with open(path) as f: data = json.load(f) for result in data['results']: filename = os.path.basename(result['bundleName']) seq = filename.split('.')[0] _, ext = os.path.splitext(filename) bundle = Bundle( seq=seq, ext=ext, bundleName=result['bundleName'], totalBytes=result['totalBytes'], files=mergeFiles(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 ''' <{element}>{status} <{element}>{path} <{element}>{ob} <{element}>{nb} <{element}>{sign}{diff} '''.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 '{body}
'.format(body='\n'.join(rows)) def printBundleDiff(bundleDiff): summary = printBundleTable([bundleDiff.total], 'th') details = printBundleTable(bundleDiff.files, 'td') return '''
{summary} {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 = [] diff = bb.totalBytes - ba.totalBytes if abs(diff) > 500: totalDiff = makeDiff(ba.bundleName, ba.totalBytes, bb.totalBytes) for aFile in ba.files: aSize = ba.files[aFile] if aFile in bb.files: bSize = bb.files[aFile] 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] 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 and ba.ext == bb.ext: found = True d = diffBundle(ba, bb) if d: diffs.append(d) break if not found: bb = Bundle( seq=ba.seq, ext=ba.ext, 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 and ba.ext == bb.ext: found = True if not found: ba = Bundle( seq=bb.seq, ext=bb.ext, 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')