├── .gitignore ├── .prettierrc.js ├── README.md ├── img ├── bundle-size-failure.png └── bundle-size-success.png ├── package-lock.json ├── package.json ├── status-checks ├── bundle-size.js ├── checks.js └── index.js └── status.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | yarn-error.log 3 | npm-debug.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Action Status Check 2 | 3 | Use Github Actions to run your own PR status checks. 4 | 5 | See the `src/status.yml` for an example action config. 6 | 7 | The script will run all checks exported from `src/checks.js`. 8 | 9 | ## How to use 10 | 11 | - Copy the `status.yml` into your `.github/workflows` 12 | - Copy `status-checks` into your project somewhere 13 | - Update the `path/to/status-checks` in `status.yml` with the correct path 14 | 15 | You can test the status check by making a PR in Github and runnning the script while on the branch. 16 | 17 | ```bash 18 | GITHUB_REPOSITORY=owner/repo GITHUB_TOKEN=YOUR_TOKEN node path/to/status-checks 19 | ``` 20 | 21 | ## Creating a check 22 | 23 | A checks should export an Object with a name and callback. 24 | 25 | ```js 26 | module.exports = { 27 | name: 'My Check'; 28 | callback: async () => 'Check passed!'; 29 | } 30 | ``` 31 | 32 | Add the check to the action by adding it to the `check` list in `src/checks`. 33 | 34 | #### Passing 35 | 36 | A check is considered successful if the callback resolves. The callback should resolve (return) a string, which will be used as the success status check description. 37 | 38 | #### Failing 39 | 40 | To fail a check, the callback can throw an Error. The error message will be used as the status description. 41 | 42 | **Example:** 43 | ```js 44 | module.exports = { 45 | name: 'My Check'; 46 | callback: async () => { 47 | throw new Error('Check failed!'); 48 | }; 49 | } 50 | ``` 51 | 52 | ## Bundle Size Example 53 | 54 | This package includes a simple bundle size status check that can measure the gzip size of files for given patterrns. 55 | 56 | It lets you set limits on bundles and pass/fail PRs based on their size. 57 | 58 | Inspired by [siddharthkp/bundlesize](https://github.com/siddharthkp/bundlesize/). 59 | 60 | ![Bundle Size PR Successfull Status Check](./img/bundle-size-success.png) 61 | 62 | ![Bundle Size PR Failing Status Check](./img/bundle-size-failure.png) 63 | -------------------------------------------------------------------------------- /img/bundle-size-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchheddles/github-action-status-check/1135a6f59174dd9f709f2d9a80db356a880a5ee4/img/bundle-size-failure.png -------------------------------------------------------------------------------- /img/bundle-size-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchheddles/github-action-status-check/1135a6f59174dd9f709f2d9a80db356a880a5ee4/img/bundle-size-success.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-action-status-check", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nodelib/fs.scandir": { 8 | "version": "2.1.3", 9 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", 10 | "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", 11 | "requires": { 12 | "@nodelib/fs.stat": "2.0.3", 13 | "run-parallel": "^1.1.9" 14 | } 15 | }, 16 | "@nodelib/fs.stat": { 17 | "version": "2.0.3", 18 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", 19 | "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" 20 | }, 21 | "@nodelib/fs.walk": { 22 | "version": "1.2.4", 23 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", 24 | "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", 25 | "requires": { 26 | "@nodelib/fs.scandir": "2.1.3", 27 | "fastq": "^1.6.0" 28 | } 29 | }, 30 | "braces": { 31 | "version": "3.0.2", 32 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 33 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 34 | "requires": { 35 | "fill-range": "^7.0.1" 36 | } 37 | }, 38 | "duplexer": { 39 | "version": "0.1.1", 40 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 41 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" 42 | }, 43 | "fast-glob": { 44 | "version": "3.1.0", 45 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.0.tgz", 46 | "integrity": "sha512-TrUz3THiq2Vy3bjfQUB2wNyPdGBeGmdjbzzBLhfHN4YFurYptCKwGq/TfiRavbGywFRzY6U2CdmQ1zmsY5yYaw==", 47 | "requires": { 48 | "@nodelib/fs.stat": "^2.0.2", 49 | "@nodelib/fs.walk": "^1.2.3", 50 | "glob-parent": "^5.1.0", 51 | "merge2": "^1.3.0", 52 | "micromatch": "^4.0.2" 53 | } 54 | }, 55 | "fastq": { 56 | "version": "1.6.0", 57 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", 58 | "integrity": "sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==", 59 | "requires": { 60 | "reusify": "^1.0.0" 61 | } 62 | }, 63 | "fill-range": { 64 | "version": "7.0.1", 65 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 66 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 67 | "requires": { 68 | "to-regex-range": "^5.0.1" 69 | } 70 | }, 71 | "glob-parent": { 72 | "version": "5.1.0", 73 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", 74 | "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", 75 | "requires": { 76 | "is-glob": "^4.0.1" 77 | } 78 | }, 79 | "gzip-size": { 80 | "version": "5.1.1", 81 | "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", 82 | "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", 83 | "requires": { 84 | "duplexer": "^0.1.1", 85 | "pify": "^4.0.1" 86 | } 87 | }, 88 | "is-extglob": { 89 | "version": "2.1.1", 90 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 91 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 92 | }, 93 | "is-glob": { 94 | "version": "4.0.1", 95 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 96 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 97 | "requires": { 98 | "is-extglob": "^2.1.1" 99 | } 100 | }, 101 | "is-number": { 102 | "version": "7.0.0", 103 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 104 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 105 | }, 106 | "merge2": { 107 | "version": "1.3.0", 108 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", 109 | "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==" 110 | }, 111 | "micromatch": { 112 | "version": "4.0.2", 113 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", 114 | "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", 115 | "requires": { 116 | "braces": "^3.0.1", 117 | "picomatch": "^2.0.5" 118 | } 119 | }, 120 | "node-fetch": { 121 | "version": "2.6.0", 122 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 123 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 124 | }, 125 | "picomatch": { 126 | "version": "2.1.1", 127 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.1.tgz", 128 | "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==" 129 | }, 130 | "pify": { 131 | "version": "4.0.1", 132 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 133 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" 134 | }, 135 | "reusify": { 136 | "version": "1.0.4", 137 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 138 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" 139 | }, 140 | "run-parallel": { 141 | "version": "1.1.9", 142 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", 143 | "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" 144 | }, 145 | "to-regex-range": { 146 | "version": "5.0.1", 147 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 148 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 149 | "requires": { 150 | "is-number": "^7.0.0" 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-action-status-check", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Mitch Heddles ", 9 | "keywords": [ 10 | "action", 11 | "status", 12 | "bundlesize" 13 | ], 14 | "license": "MIT", 15 | "dependencies": { 16 | "fast-glob": "^3.1.0", 17 | "gzip-size": "^5.1.1", 18 | "node-fetch": "^2.6.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /status-checks/bundle-size.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fg = require('fast-glob'); 3 | const gzipSize = require('gzip-size'); 4 | 5 | const BUILD_DIR = path.join(process.cwd(), 'dist'); 6 | 7 | const sizes = [ 8 | { 9 | pattern: '*.js', 10 | limit: 120 * 1024, // 120kb 11 | format: formatHashedFileName, 12 | }, 13 | { 14 | pattern: '*.css', 15 | limit: 30 * 1024, // 30kb 16 | format: formatHashedFileName, 17 | }, 18 | ]; 19 | 20 | function toKb(bytes) { 21 | return `${Math.round(bytes / 1024)}Kb`; 22 | } 23 | 24 | function formatHashedFileName(filePath) { 25 | const name = filePath.split('.')[0]; 26 | return `${name}${path.extname(filePath)}`; 27 | } 28 | 29 | function formatErrorMessage(failed) { 30 | if (failed.length === 1) { 31 | const { name, size, diff } = failed[0]; 32 | return `${name}: ${toKb(size)} (+${toKb(diff)} larger than allowed)`; 33 | } 34 | 35 | const chunkErrors = failed.map(({ name, diff }) => `${name} (+${toKb(diff)})`).join(', '); 36 | return `${failed.length} files exceed limit: ${chunkErrors}`; 37 | } 38 | 39 | async function checkBundleSize() { 40 | console.log('Starting bundle size check'); 41 | 42 | const files = []; 43 | 44 | sizes.forEach(size => { 45 | const { pattern, limit, format } = size; 46 | fg.sync(pattern, { 47 | cwd: BUILD_DIR, 48 | }).forEach(filePath => { 49 | files.push({ filePath, limit, format }); 50 | }); 51 | }); 52 | 53 | console.log(`Found ${files.length} file(s)`); 54 | 55 | const failed = []; 56 | const passed = []; 57 | 58 | await Promise.all( 59 | files.map(async chunk => { 60 | const { filePath, format, limit } = chunk; 61 | const name = typeof format === 'function' ? format(filePath) : filePath; 62 | const size = await gzipSize.file(path.join(BUILD_DIR, filePath)); 63 | 64 | console.log(`Measuring ${name} (${toKb(size)})`); 65 | 66 | const diff = size - limit; 67 | if (size > limit) { 68 | failed.push({ name, size, limit, diff }); 69 | return; 70 | } 71 | 72 | passed.push({ name, size, limit, diff }); 73 | console.log(`${name} is under size (${toKb(diff)}). Nice work!`); 74 | }), 75 | ); 76 | 77 | console.log(`Number of failures: ${failed.length}`); 78 | 79 | if (failed.length) { 80 | const errorMessage = formatErrorMessage(failed); 81 | throw new Error(errorMessage); 82 | } 83 | 84 | const chunkMessages = passed.map(({ name, size }) => `${name} (${toKb(size)})`).join(', '); 85 | 86 | console.log('Finished bundle size check'); 87 | 88 | return `Passed! ${chunkMessages}`; 89 | } 90 | 91 | module.exports = { 92 | name: 'Bundle size', 93 | callback: checkBundleSize, 94 | }; 95 | -------------------------------------------------------------------------------- /status-checks/checks.js: -------------------------------------------------------------------------------- 1 | const bundleSize = require('./bundle-size'); 2 | 3 | module.exports = [bundleSize]; 4 | -------------------------------------------------------------------------------- /status-checks/index.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | const fetch = require('node-fetch'); 3 | 4 | const checks = require('./checks'); 5 | 6 | const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); 7 | 8 | function getCurrentCommitSha() { 9 | return cp 10 | .execSync(`git rev-parse HEAD`) 11 | .toString() 12 | .trim(); 13 | } 14 | 15 | // The SHA provied by GITHUB_SHA is the merge (PR) commit. 16 | // We need to get the current commit sha ourself. 17 | const sha = getCurrentCommitSha(); 18 | 19 | async function setStatus(context, state, description) { 20 | return fetch(`https://api.github.com/repos/${owner}/${repo}/statuses/${sha}`, { 21 | method: 'POST', 22 | body: JSON.stringify({ 23 | state, 24 | description, 25 | context, 26 | }), 27 | headers: { 28 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 29 | 'Content-Type': 'application/json', 30 | }, 31 | }); 32 | } 33 | 34 | (async () => { 35 | console.log(`Starting status checks for commit ${sha}`); 36 | 37 | // Run in parallel 38 | await Promise.all( 39 | checks.map(async check => { 40 | const { name, callback } = check; 41 | 42 | await setStatus(name, 'pending', 'Running check..'); 43 | 44 | try { 45 | const response = await callback(); 46 | await setStatus(name, 'success', response); 47 | } catch (err) { 48 | const message = err ? err.message : 'Something went wrong'; 49 | await setStatus(name, 'failure', message); 50 | } 51 | }), 52 | ); 53 | 54 | console.log('Finished status checks'); 55 | })(); 56 | -------------------------------------------------------------------------------- /status.yml: -------------------------------------------------------------------------------- 1 | name: PR status checks 2 | on: 3 | pull_request: 4 | types: [assigned, opened, synchronize, reopened] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [10.x] 11 | steps: 12 | - uses: actions/checkout@v1 13 | with: 14 | # Checkout the head ref instead of the PR branch that github creates. 15 | ref: ${{ github.head_ref }} 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Install and build 21 | run: | 22 | npm install 23 | npm run build 24 | - name: Run status checks 25 | run: node path/to/status-checks 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | --------------------------------------------------------------------------------