├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── size.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── assets ├── logo.png └── pr.png ├── dist └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── SizeLimit.spec.ts ├── SizeLimit.ts ├── Term.ts └── main.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | max_line_length = 0 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/es6"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "env": { 11 | "node": true, 12 | "es6": true, 13 | "jest/globals": true 14 | }, 15 | "rules": { 16 | "no-console": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: "size" 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | size: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI_JOB_NUMBER: 1 11 | steps: 12 | - uses: actions/checkout@v1 13 | - run: | 14 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* 15 | - uses: ./ 16 | with: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - run: npm install && npm run test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andres Alvarez 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Size Limit Action 3 |

4 | 5 |

6 | A GitHub action that compares the real cost of your JavaScript in every pull request 7 |

8 |

9 | tests status 10 |

11 | 12 | This action uses [Size Limit](https://github.com/ai/size-limit) (performance budget tool for JavaScript) to calculate the real cost of your JavaScript for end-users. The main features of this action are: 13 | 14 | - **Commenting** pull requests with the comparison of Size Limit output. 15 | - **Rejecting** a pull request if the cost exceeds the limit. 16 | 17 |

18 | pr comment 19 |

20 | 21 | ## Usage 22 | 1. Install Size Limit choosing the scenario that fits you better ([JS Application](https://github.com/ai/size-limit#js-applications), [Big Libraries](https://github.com/ai/size-limit#big-libraries) or [Small Libraries](https://github.com/ai/size-limit#small-libraries)). 23 | 2. By default this action will try to build your PR by running `build` [npm script](https://docs.npmjs.com/misc/scripts) located in your `package.json`. If something need to be done after dependencies are installed but before building `postinstall` npm script could be used. For example, using [lerna](https://github.com/lerna/lerna): 24 | ```json 25 | "scripts": { 26 | "postinstall": "lerna bootstrap", 27 | "build": "lerna run build" 28 | }, 29 | ``` 30 | 3. Define Size limit configuration. For example (inside `package.json`): 31 | ```json 32 | "size-limit": [ 33 | { 34 | "path": "dist/index.js", 35 | "limit": "4500 ms" 36 | } 37 | ] 38 | ``` 39 | 4. Add the following action inside `.github/workflows/size-limit.yml` 40 | ```yaml 41 | name: "size" 42 | on: 43 | pull_request: 44 | branches: 45 | - master 46 | permissions: 47 | pull-requests: write 48 | jobs: 49 | size: 50 | runs-on: ubuntu-latest 51 | env: 52 | CI_JOB_NUMBER: 1 53 | steps: 54 | - uses: actions/checkout@v1 55 | - uses: andresz1/size-limit-action@v1 56 | with: 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | ``` 59 | You can optionally specify a custom npm script to run instead of the default `build` adding a `build_script` option to the yml workflow shown above. Additionally, providing a `skip_step` option will tell the action to skip either the `install` or `build` phase. 60 | 61 | ```yaml 62 | with: 63 | github_token: ${{ secrets.GITHUB_TOKEN }} 64 | build_script: custom-build 65 | skip_step: install 66 | ``` 67 | 68 | Add `clean_script` option to specify npm script to run after size-limit results are collected. This is useful to clean up leftover assets. 69 | 70 | ```yaml 71 | with: 72 | github_token: ${{ secrets.GITHUB_TOKEN }} 73 | clean_script: cleanup 74 | ``` 75 | 76 | 5. You are now all set 77 | 78 | ### Customizing size-limit script 79 | 80 | Use `script` option to customize the size-limit execution script (the output should always be a `json`), which defaults to `npx size-limit --json`. 81 | 82 | ```yaml 83 | with: 84 | github_token: ${{ secrets.GITHUB_TOKEN }} 85 | script: yarn dlx size-limit --json 86 | ``` 87 | 88 | ### Customizing working directory 89 | 90 | The `directory` option allow to run all the tasks in a subfolder. 91 | It's only convenient if all your stuff is in a subdirectory of your git repository. 92 | 93 | For instance, if `package.json` is in the subfolder `client/`: 94 | 95 | ```yaml 96 | with: 97 | github_token: ${{ secrets.GITHUB_TOKEN }} 98 | build_script: custom-build 99 | skip_step: install 100 | directory: client/ 101 | ``` 102 | 103 | ### Customizing the Package Manager 104 | 105 | By default, the action will attempt to autodetect which package manager to use, but in some cases 106 | like those who are using a monorepo and the directory option, this may not detect the correct 107 | manager. You can manually specify the package manager with the `package_manager` option. 108 | 109 | ```yaml 110 | with: 111 | github_token: ${{ secrets.GITHUB_TOKEN }} 112 | directory: packages/client/ 113 | package_manager: yarn 114 | ``` 115 | 116 | ## Feedback 117 | 118 | Pull requests, feature ideas and bug reports are very welcome. We highly appreciate any feedback. 119 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'size-limit-action' 2 | description: 'size-limit action' 3 | author: 'Andres Alvarez ' 4 | branding: 5 | icon: 'activity' 6 | color: 'green' 7 | inputs: 8 | github_token: 9 | required: true 10 | description: 'a github access token' 11 | build_script: 12 | required: false 13 | description: 'a custom npm script to build' 14 | clean_script: 15 | required: false 16 | description: 'a npm script to clean up build directory' 17 | skip_step: 18 | required: false 19 | description: 'which step to skip, either "install" or "build"' 20 | directory: 21 | required: false 22 | description: "a custom subdirectory" 23 | windows_verbatim_arguments: 24 | required: false 25 | description: "exec `size-limit` with the option `windowsVerbatimArguments`" 26 | default: true 27 | script: 28 | required: false 29 | default: "npx size-limit --json" 30 | description: "The script used to generate size-limit results" 31 | package_manager: 32 | required: false 33 | description: "The package manager used to run the build and install commands. If not provided, the manager will be auto detected. Example values: `yarn`, `npm`, `pnpm`, `bun`." 34 | runs: 35 | using: 'node20' 36 | main: 'dist/index.js' 37 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andresz1/size-limit-action/94bc357df29c36c8f8d50ea497c3e225c3c95d1d/assets/logo.png -------------------------------------------------------------------------------- /assets/pr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andresz1/size-limit-action/94bc357df29c36c8f8d50ea497c3e225c3c95d1d/assets/pr.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | module.exports = { 4 | clearMocks: true, 5 | moduleFileExtensions: ["js", "ts"], 6 | testEnvironment: "node", 7 | testMatch: ["**/*.spec.ts"], 8 | testRunner: "jest-circus/runner", 9 | transform: { 10 | "^.+\\.ts$": "ts-jest" 11 | }, 12 | verbose: true 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "size-limit-action", 3 | "version": "1.8.0", 4 | "private": true, 5 | "description": "size-limit action", 6 | "main": "dist/index.js", 7 | "license": "ISC", 8 | "scripts": { 9 | "format": "prettier --write **/*.ts", 10 | "lint": "eslint src/**/*.ts", 11 | "test": "jest", 12 | "build": "ncc build src/main.ts -o dist", 13 | "size-build": "npm run build", 14 | "size": "npm run size-build && size-limit" 15 | }, 16 | "size-limit": [ 17 | { 18 | "path": "dist/index.js", 19 | "limit": "4500 ms" 20 | } 21 | ], 22 | "dependencies": { 23 | "@actions/core": "^1.2.6", 24 | "@actions/exec": "^1.0.3", 25 | "@actions/github": "^2.1.1", 26 | "bytes": "^3.1.0", 27 | "has-pnpm": "^1.1.1", 28 | "has-yarn": "^2.1.0", 29 | "markdown-table": "^2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@size-limit/file": "^4.3.1", 33 | "@size-limit/time": "^4.3.1", 34 | "@types/jest": "^24.0.23", 35 | "@types/node": "^12.7.12", 36 | "@typescript-eslint/parser": "^2.8.0", 37 | "@vercel/ncc": "^0.38.1", 38 | "eslint": "^5.16.0", 39 | "eslint-plugin-github": "^2.0.0", 40 | "eslint-plugin-jest": "^22.21.0", 41 | "jest": "^24.9.0", 42 | "jest-circus": "^24.9.0", 43 | "js-yaml": "^3.13.1", 44 | "prettier": "^1.19.1", 45 | "size-limit": "^4.3.1", 46 | "ts-jest": "^24.2.0", 47 | "typescript": "^3.6.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SizeLimit.spec.ts: -------------------------------------------------------------------------------- 1 | import SizeLimit from "./SizeLimit"; 2 | 3 | describe("SizeLimit", () => { 4 | test("should parse size-limit output", () => { 5 | const limit = new SizeLimit(); 6 | const output = JSON.stringify([ 7 | { 8 | name: "dist/index.js", 9 | passed: true, 10 | size: "110894", 11 | running: "0.10210999999999999", 12 | loading: "2.1658984375" 13 | } 14 | ]); 15 | 16 | expect(limit.parseResults(output)).toEqual({ 17 | "dist/index.js": { 18 | name: "dist/index.js", 19 | loading: 2.1658984375, 20 | running: 0.10210999999999999, 21 | size: 110894, 22 | total: 2.2680084375000003 23 | } 24 | }); 25 | }); 26 | 27 | test("should parse size-limit without times output", () => { 28 | const limit = new SizeLimit(); 29 | const output = JSON.stringify([ 30 | { 31 | name: "dist/index.js", 32 | passed: true, 33 | size: "110894" 34 | } 35 | ]); 36 | 37 | expect(limit.parseResults(output)).toEqual({ 38 | "dist/index.js": { 39 | name: "dist/index.js", 40 | size: 110894 41 | } 42 | }); 43 | }); 44 | 45 | test("should format size-limit results", () => { 46 | const limit = new SizeLimit(); 47 | const base = { 48 | "dist/index.js": { 49 | name: "dist/index.js", 50 | size: 110894, 51 | running: 0.10210999999999999, 52 | loading: 2.1658984375, 53 | total: 2.2680084375000003 54 | } 55 | }; 56 | const current = { 57 | "dist/index.js": { 58 | name: "dist/index.js", 59 | size: 100894, 60 | running: 0.20210999999999999, 61 | loading: 2.5658984375, 62 | total: 2.7680084375000003 63 | } 64 | }; 65 | 66 | expect(limit.formatResults(base, current)).toEqual([ 67 | SizeLimit.TIME_RESULTS_HEADER, 68 | [ 69 | "dist/index.js", 70 | "98.53 KB (-9.02% 🔽)", 71 | "2.6 s (+18.47% 🔺)", 72 | "203 ms (+97.94% 🔺)", 73 | "2.8 s" 74 | ] 75 | ]); 76 | }); 77 | 78 | test("should format size-limit without times results", () => { 79 | const limit = new SizeLimit(); 80 | const base = { 81 | "dist/index.js": { 82 | name: "dist/index.js", 83 | size: 110894 84 | } 85 | }; 86 | const current = { 87 | "dist/index.js": { 88 | name: "dist/index.js", 89 | size: 100894 90 | } 91 | }; 92 | 93 | expect(limit.formatResults(base, current)).toEqual([ 94 | SizeLimit.SIZE_RESULTS_HEADER, 95 | ["dist/index.js", "98.53 KB (-9.02% 🔽)"] 96 | ]); 97 | }); 98 | 99 | test("should format size-limit with new section", () => { 100 | const limit = new SizeLimit(); 101 | const base = { 102 | "dist/index.js": { 103 | name: "dist/index.js", 104 | size: 110894 105 | } 106 | }; 107 | const current = { 108 | "dist/index.js": { 109 | name: "dist/index.js", 110 | size: 100894 111 | }, 112 | "dist/new.js": { 113 | name: "dist/new.js", 114 | size: 100894 115 | } 116 | }; 117 | 118 | expect(limit.formatResults(base, current)).toEqual([ 119 | SizeLimit.SIZE_RESULTS_HEADER, 120 | ["dist/index.js", "98.53 KB (-9.02% 🔽)"], 121 | ["dist/new.js", "98.53 KB (+100% 🔺)"] 122 | ]); 123 | }); 124 | 125 | test("should format size-limit with deleted section", () => { 126 | const limit = new SizeLimit(); 127 | const base = { 128 | "dist/index.js": { 129 | name: "dist/index.js", 130 | size: 110894 131 | } 132 | }; 133 | const current = { 134 | "dist/new.js": { 135 | name: "dist/new.js", 136 | size: 100894 137 | } 138 | }; 139 | 140 | expect(limit.formatResults(base, current)).toEqual([ 141 | SizeLimit.SIZE_RESULTS_HEADER, 142 | ["dist/index.js", "0 B (-100% 🔽)"], 143 | ["dist/new.js", "98.53 KB (+100% 🔺)"] 144 | ]); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/SizeLimit.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import bytes from "bytes"; 3 | 4 | interface IResult { 5 | name: string; 6 | size: number; 7 | running?: number; 8 | loading?: number; 9 | total?: number; 10 | } 11 | 12 | const EmptyResult = { 13 | name: "-", 14 | size: 0, 15 | running: 0, 16 | loading: 0, 17 | total: 0 18 | }; 19 | 20 | class SizeLimit { 21 | static SIZE_RESULTS_HEADER = ["Path", "Size"]; 22 | 23 | static TIME_RESULTS_HEADER = [ 24 | "Path", 25 | "Size", 26 | "Loading time (3g)", 27 | "Running time (snapdragon)", 28 | "Total time" 29 | ]; 30 | 31 | private formatBytes(size: number): string { 32 | return bytes.format(size, { unitSeparator: " " }); 33 | } 34 | 35 | private formatTime(seconds: number): string { 36 | if (seconds >= 1) { 37 | return `${Math.ceil(seconds * 10) / 10} s`; 38 | } 39 | 40 | return `${Math.ceil(seconds * 1000)} ms`; 41 | } 42 | 43 | private formatChange(base: number = 0, current: number = 0): string { 44 | if (base === 0) { 45 | return "+100% 🔺"; 46 | } 47 | 48 | const value = ((current - base) / base) * 100; 49 | const formatted = 50 | (Math.sign(value) * Math.ceil(Math.abs(value) * 100)) / 100; 51 | 52 | if (value > 0) { 53 | return `+${formatted}% 🔺`; 54 | } 55 | 56 | if (value === 0) { 57 | return `${formatted}%`; 58 | } 59 | 60 | return `${formatted}% 🔽`; 61 | } 62 | 63 | private formatLine(value: string, change: string) { 64 | return `${value} (${change})`; 65 | } 66 | 67 | private formatSizeResult( 68 | name: string, 69 | base: IResult, 70 | current: IResult 71 | ): Array { 72 | return [ 73 | name, 74 | this.formatLine( 75 | this.formatBytes(current.size), 76 | this.formatChange(base.size, current.size) 77 | ) 78 | ]; 79 | } 80 | 81 | private formatTimeResult( 82 | name: string, 83 | base: IResult, 84 | current: IResult 85 | ): Array { 86 | return [ 87 | name, 88 | this.formatLine( 89 | this.formatBytes(current.size), 90 | this.formatChange(base.size, current.size) 91 | ), 92 | this.formatLine( 93 | this.formatTime(current.loading), 94 | this.formatChange(base.loading, current.loading) 95 | ), 96 | this.formatLine( 97 | this.formatTime(current.running), 98 | this.formatChange(base.running, current.running) 99 | ), 100 | this.formatTime(current.total) 101 | ]; 102 | } 103 | 104 | parseResults(output: string): { [name: string]: IResult } { 105 | const results = JSON.parse(output); 106 | 107 | return results.reduce( 108 | (current: { [name: string]: IResult }, result: any) => { 109 | let time = {}; 110 | 111 | if (result.loading !== undefined && result.running !== undefined) { 112 | const loading = +result.loading; 113 | const running = +result.running; 114 | 115 | time = { 116 | running, 117 | loading, 118 | total: loading + running 119 | }; 120 | } 121 | 122 | return { 123 | ...current, 124 | [result.name]: { 125 | name: result.name, 126 | size: +result.size, 127 | ...time 128 | } 129 | }; 130 | }, 131 | {} 132 | ); 133 | } 134 | 135 | formatResults( 136 | base: { [name: string]: IResult }, 137 | current: { [name: string]: IResult } 138 | ): Array> { 139 | const names = [...new Set([...Object.keys(base), ...Object.keys(current)])]; 140 | const isSize = names.some( 141 | (name: string) => current[name] && current[name].total === undefined 142 | ); 143 | const header = isSize 144 | ? SizeLimit.SIZE_RESULTS_HEADER 145 | : SizeLimit.TIME_RESULTS_HEADER; 146 | const fields = names.map((name: string) => { 147 | const baseResult = base[name] || EmptyResult; 148 | const currentResult = current[name] || EmptyResult; 149 | 150 | if (isSize) { 151 | return this.formatSizeResult(name, baseResult, currentResult); 152 | } 153 | return this.formatTimeResult(name, baseResult, currentResult); 154 | }); 155 | 156 | return [header, ...fields]; 157 | } 158 | } 159 | export default SizeLimit; 160 | -------------------------------------------------------------------------------- /src/Term.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "@actions/exec"; 2 | import hasYarn from "has-yarn"; 3 | import hasPNPM from "has-pnpm"; 4 | 5 | import process from 'process'; 6 | import path from 'path'; 7 | import fs from 'fs'; 8 | 9 | function hasBun(cwd = process.cwd()) { 10 | return fs.existsSync(path.resolve(cwd, 'bun.lockb')); 11 | } 12 | 13 | const INSTALL_STEP = "install"; 14 | const BUILD_STEP = "build"; 15 | 16 | class Term { 17 | /** 18 | * Autodetects and gets the current package manager for the current directory, either yarn, pnpm, bun, 19 | * or npm. Default is `npm`. 20 | * 21 | * @param directory The current directory 22 | * @returns The detected package manager in use, one of `yarn`, `pnpm`, `npm`, `bun` 23 | */ 24 | getPackageManager(directory?: string): string { 25 | return hasYarn(directory) ? "yarn" : hasPNPM(directory) ? "pnpm" : hasBun(directory) ? "bun" : "npm"; 26 | } 27 | 28 | async execSizeLimit( 29 | branch?: string, 30 | skipStep?: string, 31 | buildScript?: string, 32 | cleanScript?: string, 33 | windowsVerbatimArguments?: boolean, 34 | directory?: string, 35 | script?: string, 36 | packageManager?: string 37 | ): Promise<{ status: number; output: string }> { 38 | const manager = packageManager || this.getPackageManager(directory); 39 | let output = ""; 40 | 41 | if (branch) { 42 | try { 43 | await exec(`git fetch origin ${branch} --depth=1`); 44 | } catch (error) { 45 | console.log("Fetch failed", error.message); 46 | } 47 | 48 | await exec(`git checkout -f ${branch}`); 49 | } 50 | 51 | if (skipStep !== INSTALL_STEP && skipStep !== BUILD_STEP) { 52 | await exec(`${manager} install`, [], { 53 | cwd: directory 54 | }); 55 | } 56 | 57 | if (skipStep !== BUILD_STEP) { 58 | const script = buildScript || "build"; 59 | await exec(`${manager} run ${script}`, [], { 60 | cwd: directory 61 | }); 62 | } 63 | 64 | const status = await exec(script, [], { 65 | windowsVerbatimArguments, 66 | ignoreReturnCode: true, 67 | listeners: { 68 | stdout: (data: Buffer) => { 69 | output += data.toString(); 70 | } 71 | }, 72 | cwd: directory 73 | }); 74 | 75 | if (cleanScript) { 76 | await exec(`${manager} run ${cleanScript}`, [], { 77 | cwd: directory 78 | }); 79 | } 80 | 81 | return { 82 | status, 83 | output 84 | }; 85 | } 86 | } 87 | 88 | export default Term; 89 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { getInput, setFailed } from "@actions/core"; 2 | import { context, GitHub } from "@actions/github"; 3 | // @ts-ignore 4 | import table from "markdown-table"; 5 | import Term from "./Term"; 6 | import SizeLimit from "./SizeLimit"; 7 | 8 | const SIZE_LIMIT_HEADING = `## size-limit report 📦 `; 9 | 10 | async function fetchPreviousComment( 11 | octokit: GitHub, 12 | repo: { owner: string; repo: string }, 13 | pr: { number: number } 14 | ) { 15 | // TODO: replace with octokit.issues.listComments when upgraded to v17 16 | const commentList = await octokit.paginate( 17 | "GET /repos/:owner/:repo/issues/:issue_number/comments", 18 | { 19 | ...repo, 20 | // eslint-disable-next-line camelcase 21 | issue_number: pr.number 22 | } 23 | ); 24 | 25 | const sizeLimitComment = commentList.find(comment => 26 | comment.body.startsWith(SIZE_LIMIT_HEADING) 27 | ); 28 | return !sizeLimitComment ? null : sizeLimitComment; 29 | } 30 | 31 | async function run() { 32 | try { 33 | const { payload, repo } = context; 34 | const pr = payload.pull_request; 35 | 36 | if (!pr) { 37 | throw new Error( 38 | "No PR found. Only pull_request workflows are supported." 39 | ); 40 | } 41 | 42 | const token = getInput("github_token"); 43 | const skipStep = getInput("skip_step"); 44 | const buildScript = getInput("build_script"); 45 | const cleanScript = getInput("clean_script"); 46 | const script = getInput("script"); 47 | const packageManager = getInput("package_manager"); 48 | const directory = getInput("directory") || process.cwd(); 49 | const windowsVerbatimArguments = 50 | getInput("windows_verbatim_arguments") === "true" ? true : false; 51 | const octokit = new GitHub(token); 52 | const term = new Term(); 53 | const limit = new SizeLimit(); 54 | 55 | const { status, output } = await term.execSizeLimit( 56 | null, 57 | skipStep, 58 | buildScript, 59 | cleanScript, 60 | windowsVerbatimArguments, 61 | directory, 62 | script, 63 | packageManager 64 | ); 65 | const { output: baseOutput } = await term.execSizeLimit( 66 | pr.base.ref, 67 | null, 68 | buildScript, 69 | cleanScript, 70 | windowsVerbatimArguments, 71 | directory, 72 | script, 73 | packageManager 74 | ); 75 | 76 | let base; 77 | let current; 78 | 79 | try { 80 | base = limit.parseResults(baseOutput); 81 | current = limit.parseResults(output); 82 | } catch (error) { 83 | console.log( 84 | "Error parsing size-limit output. The output should be a json." 85 | ); 86 | throw error; 87 | } 88 | 89 | const body = [ 90 | SIZE_LIMIT_HEADING, 91 | table(limit.formatResults(base, current)) 92 | ].join("\r\n"); 93 | 94 | const sizeLimitComment = await fetchPreviousComment(octokit, repo, pr); 95 | 96 | if (!sizeLimitComment) { 97 | try { 98 | await octokit.issues.createComment({ 99 | ...repo, 100 | // eslint-disable-next-line camelcase 101 | issue_number: pr.number, 102 | body 103 | }); 104 | } catch (error) { 105 | console.log( 106 | "Error creating comment. This can happen for PR's originating from a fork without write permissions." 107 | ); 108 | } 109 | } else { 110 | try { 111 | await octokit.issues.updateComment({ 112 | ...repo, 113 | // eslint-disable-next-line camelcase 114 | comment_id: sizeLimitComment.id, 115 | body 116 | }); 117 | } catch (error) { 118 | console.log( 119 | "Error updating comment. This can happen for PR's originating from a fork without write permissions." 120 | ); 121 | } 122 | } 123 | 124 | if (status > 0) { 125 | setFailed("Size limit has been exceeded."); 126 | } 127 | } catch (error) { 128 | setFailed(error.message); 129 | } 130 | } 131 | 132 | run(); 133 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": false, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | "moduleResolution": "node" 11 | }, 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | --------------------------------------------------------------------------------