├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── .kodiak.toml ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── fossa.yml │ ├── labeler.yml │ ├── prepare.yml │ ├── release-please.yml │ ├── versioning.yml │ └── workflow.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── commitlint.config.cjs ├── dist ├── index.js └── index.js.map ├── package-lock.json ├── package.json ├── renovate.json5 ├── scripts └── build.js ├── src ├── index.js └── lib │ ├── comment.js │ ├── delta_file.js │ ├── github.js │ ├── graph.js │ ├── inputs.js │ └── units.js └── test └── unit └── lib ├── comment.js └── snapshots ├── comment.js.md └── comment.js.snap /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { overrides } = require('@netlify/eslint-config-node/.eslintrc_esm.cjs') 4 | 5 | module.exports = { 6 | extends: '@netlify/eslint-config-node/.eslintrc_esm.cjs', 7 | overrides: [...overrides], 8 | } 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge.automerge_dependencies] 4 | versions = ["minor", "patch"] 5 | usernames = ["renovate"] 6 | 7 | [approve] 8 | auto_approve_usernames = ["renovate"] -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/CODEOWNERS @netlify/netlify-dev 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Please replace with a clear and descriptive title' 5 | labels: 'type: bug' 6 | assignees: '' 7 | --- 8 | 9 | Thanks for reporting this bug! 10 | 11 | Please search other issues to make sure this bug has not already been reported. 12 | 13 | Then fill in the sections below. 14 | 15 | **Describe the bug** 16 | 17 | A clear and concise description of what the bug is. 18 | 19 | **Configuration** 20 | 21 | Please enter the following command in a terminal and copy/paste its output: 22 | 23 | ```bash 24 | npx envinfo --system --binaries 25 | ``` 26 | 27 | **Pull requests** 28 | 29 | Pull requests are welcome! If you would like to help us fix this bug, please check our 30 | [contributions guidelines](../blob/main/CONTRIBUTING.md). 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Please replace with a clear and descriptive title' 5 | labels: 'type: feature' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | **Which problem is this feature request solving?** 16 | 17 | 20 | 21 | **Describe the solution you'd like** 22 | 23 | 26 | 27 | **Describe alternatives you've considered** 28 | 29 | 32 | 33 | **Can you submit a pull request?** 34 | 35 | Yes/No. 36 | 37 | 41 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 🎉 Thanks for submitting a pull request! 🎉 2 | 3 | #### Summary 4 | 5 | Fixes # 6 | 7 | 10 | 11 | --- 12 | 13 | For us to review and ship your PR efficiently, please perform the following steps: 14 | 15 | - [ ] Open a [bug/issue](https://github.com/netlify/delta-action/issues/new/choose) before writing your code 🧑‍💻. This 16 | ensures we can discuss the changes and get feedback from everyone that should be involved. If you\`re fixing a 17 | typo or something that\`s on fire 🔥 (e.g. incident related), you can skip this step. 18 | - [ ] Read the [contribution guidelines](../CONTRIBUTING.md) 📖. This ensures your code follows our style guide and 19 | passes our tests. 20 | - [ ] Update or add tests (if any source code was changed or added) 🧪 21 | - [ ] Update or add documentation (if features were changed or added) 📝 22 | - [ ] Make sure the status checks below are successful ✅ 23 | 24 | **A picture of a cute animal (not mandatory, but encouraged)** 25 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: Dependency License Scanning 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - chore/fossa-workflow 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | fossa: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Download fossa cli 20 | run: |- 21 | mkdir -p $HOME/.local/bin 22 | curl https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash -s -- -b $HOME/.local/bin 23 | echo "$HOME/.local/bin" >> $GITHUB_PATH 24 | 25 | - name: Fossa init 26 | run: fossa init 27 | - name: Set env 28 | run: echo "line_number=$(grep -n "project" .fossa.yml | cut -f1 -d:)" >> $GITHUB_ENV 29 | - name: Configuration 30 | run: |- 31 | sed -i "${line_number}s|.*| project: git@github.com:${GITHUB_REPOSITORY}.git|" .fossa.yml 32 | cat .fossa.yml 33 | - name: Upload dependencies 34 | run: fossa analyze --debug 35 | env: 36 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 37 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Label PR 2 | on: 3 | pull_request: 4 | types: [opened, edited] 5 | 6 | jobs: 7 | label-pr: 8 | if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | pr: 13 | [ 14 | { prefix: 'fix', type: 'bug' }, 15 | { prefix: 'chore', type: 'chore' }, 16 | { prefix: 'test', type: 'chore' }, 17 | { prefix: 'ci', type: 'chore' }, 18 | { prefix: 'feat', type: 'feature' }, 19 | { prefix: 'security', type: 'security' }, 20 | ] 21 | steps: 22 | - uses: netlify/pr-labeler-action@v1.1.0 23 | if: startsWith(github.event.pull_request.title, matrix.pr.prefix) 24 | with: 25 | token: '${{ secrets.GITHUB_TOKEN }}' 26 | label: 'type: ${{ matrix.pr.type }}' 27 | -------------------------------------------------------------------------------- /.github/workflows/prepare.yml: -------------------------------------------------------------------------------- 1 | name: prepare-dist 2 | on: 3 | push: 4 | branches: 5 | - release-* 6 | jobs: 7 | prepare-dist: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: navikt/github-app-token-generator@a8ae52448279d468cfbca5cd899f2457f0b1f643 11 | id: get-token 12 | with: 13 | private-key: ${{ secrets.TOKENS_PRIVATE_KEY }} 14 | app-id: ${{ secrets.TOKENS_APP_ID }} 15 | - uses: actions/checkout@v3 16 | with: 17 | token: ${{ steps.get-token.outputs.token }} 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: '*' 21 | cache: 'npm' 22 | check-latest: true 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Prepare dist 26 | run: npm run prepare 27 | - uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | commit_message: 'chore: prepare dist' 30 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: navikt/github-app-token-generator@a8ae52448279d468cfbca5cd899f2457f0b1f643 11 | id: get-token 12 | with: 13 | private-key: ${{ secrets.TOKENS_PRIVATE_KEY }} 14 | app-id: ${{ secrets.TOKENS_APP_ID }} 15 | - uses: GoogleCloudPlatform/release-please-action@v3 16 | with: 17 | token: ${{ steps.get-token.outputs.token }} 18 | release-type: node 19 | package-name: '@netlify/delta-action' 20 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | name: Keep the versions up-to-date 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | actions-tagger: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: Actions-R-Us/actions-tagger@latest 12 | with: 13 | publish_latest_tag: true 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | # Ensure GitHub actions are not run twice for same commits 4 | push: 5 | branches: [main] 6 | tags: ['*'] 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | timeout-minutes: 30 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macOS-latest, windows-latest] 16 | fail-fast: false 17 | steps: 18 | - name: Git checkout 19 | uses: actions/checkout@v3 20 | - name: Using Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '16' 24 | cache: 'npm' 25 | check-latest: true 26 | - name: Install dependencies 27 | run: npm ci 28 | - name: Linting 29 | run: npm run format:ci 30 | - name: Tests 31 | run: npm run test:ci 32 | - name: Get test coverage flags 33 | id: test-coverage-flags 34 | run: |- 35 | os=${{ matrix.os }} 36 | echo "::set-output name=os::${os/-latest/}" 37 | shell: bash 38 | - uses: codecov/codecov-action@v3 39 | with: 40 | file: coverage/coverage-final.json 41 | flags: ${{ steps.test-coverage-flags.outputs.os }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | npm-debug.log 4 | node_modules 5 | /core 6 | .eslintcache 7 | .npmrc 8 | .yarn-error.log 9 | /coverage 10 | /build 11 | .vscode 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@netlify/eslint-config-node/.prettierrc.json" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.1.0](https://github.com/netlify/delta-action/compare/v4.0.3...v4.1.0) (2022-11-24) 4 | 5 | 6 | ### Features 7 | 8 | * make graphs optional ([#331](https://github.com/netlify/delta-action/issues/331)) ([38e4750](https://github.com/netlify/delta-action/commit/38e47500db147312c4e9bc24233afae6fcea1a66)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency @actions/core to v1.10.0 ([#323](https://github.com/netlify/delta-action/issues/323)) ([148539a](https://github.com/netlify/delta-action/commit/148539a3cf0aebdd69fbdbaa4f8af870e358c676)) 14 | * **deps:** update dependency @actions/core to v1.9.1 ([#315](https://github.com/netlify/delta-action/issues/315)) ([a4a093b](https://github.com/netlify/delta-action/commit/a4a093b7b0ad1b06760b11a791751838223cce36)) 15 | * **deps:** update dependency @actions/github to v5.1.1 ([#324](https://github.com/netlify/delta-action/issues/324)) ([31f8709](https://github.com/netlify/delta-action/commit/31f870981bbe8d4cafe487ab75070a9ec6fca94e)) 16 | 17 | ## [4.0.3](https://github.com/netlify/delta-action/compare/v4.0.2...v4.0.3) (2022-07-11) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * support custom PR numbers for workflow_run ([#308](https://github.com/netlify/delta-action/issues/308)) ([213d85b](https://github.com/netlify/delta-action/commit/213d85b01c9d810d66bd87214409ba870a715f26)) 23 | 24 | ## [4.0.2](https://github.com/netlify/delta-action/compare/v4.0.1...v4.0.2) (2022-07-08) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * read eventName from correct payload ([#304](https://github.com/netlify/delta-action/issues/304)) ([1d4d398](https://github.com/netlify/delta-action/commit/1d4d39837147c6921105f75fa63f9c1efdd392f0)) 30 | 31 | ## [4.0.1](https://github.com/netlify/delta-action/compare/v4.0.0...v4.0.1) (2022-07-07) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * detect pull-requests correctly ([#302](https://github.com/netlify/delta-action/issues/302)) ([7edd063](https://github.com/netlify/delta-action/commit/7edd063f7b29991ea0fccb93c165ebfe7d44d450)) 37 | 38 | ## [4.0.0](https://github.com/netlify/delta-action/compare/v3.0.2...v4.0.0) (2022-07-07) 39 | 40 | 41 | ### ⚠ BREAKING CHANGES 42 | 43 | * The runner now uses node 16 44 | 45 | ### Features 46 | 47 | * support workflow_run event and use node 16 ([#300](https://github.com/netlify/delta-action/issues/300)) ([5f20f0c](https://github.com/netlify/delta-action/commit/5f20f0c7486ade3a35f5b302e93ba02b1b2ce381)) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **deps:** update dependency @actions/core to v1.9.0 ([#295](https://github.com/netlify/delta-action/issues/295)) ([55e940b](https://github.com/netlify/delta-action/commit/55e940b31ab9b903a9d32cd334511b9bab151856)) 53 | * **deps:** update dependency pretty-ms to v8 ([#293](https://github.com/netlify/delta-action/issues/293)) ([8007bf5](https://github.com/netlify/delta-action/commit/8007bf59f3c29ab8cb328854191ae07900db35b9)) 54 | * order of commits in PR comment ([#301](https://github.com/netlify/delta-action/issues/301)) ([d2210d2](https://github.com/netlify/delta-action/commit/d2210d2497c2a240b38aa2ae51585883d390309b)) 55 | 56 | ## [3.0.2](https://github.com/netlify/delta-action/compare/v3.0.1...v3.0.2) (2022-06-02) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * correctly draw missing metrics from previous commits ([#290](https://github.com/netlify/delta-action/issues/290)) ([305594b](https://github.com/netlify/delta-action/commit/305594ba187ccb9ea224f6cad99df2ca7f9a3992)) 62 | * **deps:** update dependency @actions/core to v1.7.0 ([#280](https://github.com/netlify/delta-action/issues/280)) ([5b22d94](https://github.com/netlify/delta-action/commit/5b22d94da0ea94c30fe3b65ed1457fa7e2df6677)) 63 | * **deps:** update dependency @actions/core to v1.8.0 ([#282](https://github.com/netlify/delta-action/issues/282)) ([52943b1](https://github.com/netlify/delta-action/commit/52943b1287871c272b8ce4be985d66dd39f2e757)) 64 | * **deps:** update dependency @actions/core to v1.8.2 ([#287](https://github.com/netlify/delta-action/issues/287)) ([9230ecf](https://github.com/netlify/delta-action/commit/9230ecfb310a7c0b0d8f3bd845f5f54e3e6218f0)) 65 | * **deps:** update dependency @actions/github to v5.0.3 ([#288](https://github.com/netlify/delta-action/issues/288)) ([9023f48](https://github.com/netlify/delta-action/commit/9023f482c816a05b2e6f21b4136680d1b86a04ba)) 66 | 67 | ### [3.0.1](https://github.com/netlify/delta-action/compare/v3.0.0...v3.0.1) (2022-03-07) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * output ESM ([#266](https://github.com/netlify/delta-action/issues/266)) ([77b4183](https://github.com/netlify/delta-action/commit/77b41839f7eb8486282ba762bf4f0a1bd801e39e)) 73 | 74 | ## [3.0.0](https://www.github.com/netlify/delta-action/compare/v2.0.0...v3.0.0) (2021-12-09) 75 | 76 | 77 | ### ⚠ BREAKING CHANGES 78 | 79 | * use pure ES modules (#177) 80 | 81 | ### Miscellaneous Chores 82 | 83 | * use pure ES modules ([#177](https://www.github.com/netlify/delta-action/issues/177)) ([2ead9e6](https://www.github.com/netlify/delta-action/commit/2ead9e68a08ad380c06c82754fd86ff12c5d41a7)) 84 | 85 | ## [2.0.0](https://www.github.com/netlify/delta-action/compare/v1.3.0...v2.0.0) (2021-11-25) 86 | 87 | 88 | ### ⚠ BREAKING CHANGES 89 | 90 | * drop support for Node 8 and 10 (#159) 91 | 92 | ### Bug Fixes 93 | 94 | * **deps:** update dependency @actions/core to v1.6.0 ([#145](https://www.github.com/netlify/delta-action/issues/145)) ([44e422a](https://www.github.com/netlify/delta-action/commit/44e422a27d3f9319ac2b04e326147da9fd189626)) 95 | 96 | 97 | ### Miscellaneous Chores 98 | 99 | * drop support for Node 8 and 10 ([#159](https://www.github.com/netlify/delta-action/issues/159)) ([5cb7e98](https://www.github.com/netlify/delta-action/commit/5cb7e981de742c1338679b8ba4bd384df25dbc77)) 100 | 101 | ## [1.3.0](https://www.github.com/netlify/delta-action/compare/v1.2.4...v1.3.0) (2021-06-27) 102 | 103 | 104 | ### Features 105 | 106 | * draw mean in graph ([#86](https://www.github.com/netlify/delta-action/issues/86)) ([5209da8](https://www.github.com/netlify/delta-action/commit/5209da859cd9d795920b591885555e88cc22bf0b)) 107 | 108 | ### [1.2.4](https://www.github.com/netlify/delta-action/compare/v1.2.3...v1.2.4) (2021-06-22) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * account for missing metrics in the base branch ([#80](https://www.github.com/netlify/delta-action/issues/80)) ([54a4530](https://www.github.com/netlify/delta-action/commit/54a45304746b67afe54e429011eda5a64823aa4b)) 114 | 115 | ### [1.2.3](https://www.github.com/netlify/delta-action/compare/v1.2.2...v1.2.3) (2021-05-31) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * show most recent items in graph ([#66](https://www.github.com/netlify/delta-action/issues/66)) ([233b983](https://www.github.com/netlify/delta-action/commit/233b9830c97fbe43f5eae0e519093a2f8ba2cd2d)) 121 | 122 | ### [1.2.2](https://www.github.com/netlify/delta-action/compare/v1.2.1...v1.2.2) (2021-05-31) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * reduce max graph items ([#64](https://www.github.com/netlify/delta-action/issues/64)) ([8b11ffa](https://www.github.com/netlify/delta-action/commit/8b11ffa14b67766d64a337264846148bcb3fd356)) 128 | 129 | ### [1.2.1](https://www.github.com/netlify/delta-action/compare/v1.2.0...v1.2.1) (2021-05-30) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * improve graph rendering ([#58](https://www.github.com/netlify/delta-action/issues/58)) ([77c1a67](https://www.github.com/netlify/delta-action/commit/77c1a6747d9d9db5694b206fcf80f5004a9ded7c)) 135 | 136 | ## [1.2.0](https://www.github.com/netlify/delta-action/compare/v1.1.0...v1.2.0) (2021-05-30) 137 | 138 | 139 | ### Features 140 | 141 | * add graph for historical data ([#54](https://www.github.com/netlify/delta-action/issues/54)) ([ff1ba70](https://www.github.com/netlify/delta-action/commit/ff1ba70b51984c1e46e6341589b6a6f18a825ad2)) 142 | * add graph for historical data ([#57](https://www.github.com/netlify/delta-action/issues/57)) ([d477357](https://www.github.com/netlify/delta-action/commit/d4773571d884d6aa70aa09322dbaea5ed2dd91c8)) 143 | 144 | ## [1.1.0](https://www.github.com/netlify/delta-action/compare/v1.0.4...v1.1.0) (2021-05-27) 145 | 146 | 147 | ### Features 148 | 149 | * add historic data to metadata comments ([#52](https://www.github.com/netlify/delta-action/issues/52)) ([24ab1e6](https://www.github.com/netlify/delta-action/commit/24ab1e6953362abcd3fe59d44d1807942415c61d)) 150 | 151 | ### [1.0.4](https://www.github.com/netlify/delta-action/compare/v1.0.3...v1.0.4) (2021-05-24) 152 | 153 | 154 | ### Bug Fixes 155 | 156 | * **deps:** update dependency @actions/core to v1.3.0 ([#48](https://www.github.com/netlify/delta-action/issues/48)) ([d7cd7b3](https://www.github.com/netlify/delta-action/commit/d7cd7b3899900fae77d7af7cae49d19ff4d9efef)) 157 | 158 | ### [1.0.3](https://www.github.com/netlify/delta-action/compare/v1.0.2...v1.0.3) (2021-05-19) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * **deps:** update dependency @actions/github to v5 ([#39](https://www.github.com/netlify/delta-action/issues/39)) ([e941254](https://www.github.com/netlify/delta-action/commit/e94125470d497623109de7aa2a70d3e6e245a4cd)) 164 | 165 | ### [1.0.2](https://www.github.com/netlify/delta-action/compare/v1.0.1...v1.0.2) (2021-04-20) 166 | 167 | 168 | ### Bug Fixes 169 | 170 | * use correct format in comment metadata ([#18](https://www.github.com/netlify/delta-action/issues/18)) ([badb5f6](https://www.github.com/netlify/delta-action/commit/badb5f6f531a9681e14c46d8a06ec68eb41b23bf)) 171 | 172 | ### [1.0.1](https://www.github.com/netlify/delta-action/compare/v1.0.0...v1.0.1) (2021-04-19) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * **deps:** update dependency pretty-ms to v7 ([#9](https://www.github.com/netlify/delta-action/issues/9)) ([3045e10](https://www.github.com/netlify/delta-action/commit/3045e106c469ac7a5dba130511c4baac8ba1877a)) 178 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | david@netlify.com. All complaints will be reviewed and investigated and will result in a response that is deemed 48 | necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to 49 | the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [http://contributor-covenant.org/version/1/4][version] 58 | 59 | [homepage]: http://contributor-covenant.org 60 | [version]: http://contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | 3 | 🎉 Thanks for considering contributing to this project! 🎉 4 | 5 | These guidelines will help you send a pull request. 6 | 7 | If you're submitting an issue instead, please skip this document. 8 | 9 | If your pull request is related to a typo or the documentation being unclear, please click on the relevant page's `Edit` 10 | button (pencil icon) and directly suggest a correction instead. 11 | 12 | This project was made with ❤️. The simplest way to give back is by starring and sharing it online. 13 | 14 | Everyone is welcome regardless of personal background. We enforce a [Code of conduct](CODE_OF_CONDUCT.md) in order to 15 | promote a positive and inclusive environment. 16 | 17 | ## Development process 18 | 19 | First fork and clone the repository. If you're not sure how to do this, please watch 20 | [these videos](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 21 | 22 | Run: 23 | 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | To prepare the code for distribution, run: 29 | 30 | ```bash 31 | npm run prepare 32 | ``` 33 | 34 | After submitting the pull request, please make sure the Continuous Integration checks are passing. 35 | 36 | ## Releasing 37 | 38 | Merge the release PR 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Netlify 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delta Action 2 | 3 | A GitHub Action for reporting benchmark data and comparing it against a baseline. 4 | 5 | Screenshot of a benchmark report comment 6 | 7 | ## The basics 8 | 9 | This action reads benchmark metrics on GitHub pull requests and commits, and reports them by adding a comment with any 10 | metrics found. It also compares then against the latest commit on the main branch, treating it as the baseline. 11 | 12 | The action looks for benchmark data in files on the repository root. These should be named in the format 13 | `.delta.` — e.g. `.delta.install_time` will create a metric called `install_time`. 14 | 15 | These files should contain: 16 | 17 | - A number representing the value of the metric 18 | - The units of the metric (optional) 19 | - A human-friendly name of the metric (optional) 20 | 21 | _Example: `.delta.install_time`_ 22 | 23 | ``` 24 | 350ms (Installation time) 25 | ``` 26 | 27 | The units will determine how the values will be formatted in the benchmark reports. Supported units are: 28 | 29 | - Time (formatted with [`pretty-ms`](https://www.npmjs.com/package/pretty-ms)) 30 | - `ms` / `milliseconds` 31 | - `s` / `seconds` 32 | - Storage (formatted with [`pretty-bytes`](https://www.npmjs.com/package/pretty-bytes)) 33 | - `b` / `bytes` 34 | - `kb` / `kilobytes` 35 | - Unitless (formatted with 36 | [`Number.prototype.toLocaleString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString)) 37 | 38 | ## Configuration 39 | 40 | The action supports the following optional inputs: 41 | 42 | | Name | Description | Default | 43 | | ------------- | ------------------------------------------------------------------------- | ------------- | 44 | | `base_branch` | Name of the base branch, if not auto detected | autodetect | 45 | | `title` | Title/heading to include in the comments | Delta results | 46 | | `token` | GitHub access token | GITHUB_TOKEN | 47 | | `pr_number` | The PR this run is associated with (for `workflow_run`) | autodetect | 48 | | `style` | The rendering style to use when commenting on PRs (options: text, graph) | graph | 49 | 50 | ## Usage 51 | 52 | 1. Add a benchmark step to your workflow that creates a `.delta.` file with the format described above 53 | 54 | ```yaml 55 | - name: Run benchmark 56 | run: echo 123ms > .delta.install_time 57 | ``` 58 | 59 | 2. Add the action to the workflow 60 | 61 | ```yaml 62 | - name: Delta 63 | uses: netlify/delta-action@v3 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | ``` 67 | 68 | ## Contributors 69 | 70 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on how to set up and work on this repository. Thanks 71 | for contributing! 72 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Delta' 2 | description: 'A GitHub Action for capturing benchmark data and tracking its variation against a baseline' 3 | inputs: 4 | base_branch: 5 | default: main 6 | description: 'Name of the base branch' 7 | required: false 8 | title: 9 | default: Delta results 10 | description: 'Title used in the comments' 11 | required: false 12 | token: 13 | description: 'GitHub access token' 14 | required: false 15 | default: ${{ github.token }} 16 | pr_number: 17 | description: 'The PR this run is associated with (for `workflow_run`)' 18 | required: false 19 | default: '' 20 | style: 21 | description: 'The rendering style to use when commenting on PRs (options: text, graph)' 22 | required: false 23 | default: 'graph' 24 | runs: 25 | using: 'node16' 26 | main: 'dist/index.js' 27 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { extends: ['@commitlint/config-conventional'] } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netlify/delta-action", 3 | "private": true, 4 | "version": "4.1.0", 5 | "description": "A GitHub Action for capturing benchmark data and tracking its variation against a baseline", 6 | "type": "module", 7 | "exports": "./src/index.js", 8 | "main": "./src/index.js", 9 | "files": [ 10 | "src/**/*.js", 11 | "!src/**/*.test.js" 12 | ], 13 | "scripts": { 14 | "build": "node scripts/build.js", 15 | "prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/ && npm run build", 16 | "test": "run-s format test:dev", 17 | "format": "run-s format:check-fix:*", 18 | "format:ci": "run-s format:check:*", 19 | "format:check-fix:lint": "run-e format:check:lint format:fix:lint", 20 | "format:check:lint": "cross-env-shell eslint $npm_package_config_eslint", 21 | "format:fix:lint": "cross-env-shell eslint --fix $npm_package_config_eslint", 22 | "format:check-fix:prettier": "run-e format:check:prettier format:fix:prettier", 23 | "format:check:prettier": "cross-env-shell prettier --check $npm_package_config_prettier", 24 | "format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier", 25 | "test:dev": "run-s test:dev:*", 26 | "test:ci": "run-s test:ci:*", 27 | "test:dev:ava": "ava", 28 | "test:ci:ava": "c8 -r lcovonly -r text -r json ava" 29 | }, 30 | "config": { 31 | "eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github}/**/*.{cjs,mjs,js,md,html}\" \"*.{cjs,mjs,js,md,html}\" \".*.{cjs,mjs,js,md,html}\"", 32 | "prettier": "--ignore-path .gitignore --loglevel=warn \"{src,scripts,.github}/**/*.{cjs,mjs,js,md,yml,json,html}\" \"*.{cjs,mjs,js,yml,json,html}\" \".*.{cjs,mjs,js,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\"" 33 | }, 34 | "eslintIgnore": [ 35 | "dist/*" 36 | ], 37 | "ava": { 38 | "verbose": true 39 | }, 40 | "keywords": [], 41 | "license": "MIT", 42 | "repository": "netlify/delta-action", 43 | "bugs": { 44 | "url": "https://github.com/netlify/delta-action/issues" 45 | }, 46 | "author": "Netlify Inc.", 47 | "directories": { 48 | "test": "test" 49 | }, 50 | "dependencies": { 51 | "@actions/core": "^1.9.0", 52 | "@actions/github": "^5.0.3", 53 | "pretty-bytes": "^5.6.0", 54 | "pretty-ms": "^8.0.0", 55 | "regex-escape": "^3.4.10" 56 | }, 57 | "devDependencies": { 58 | "@netlify/eslint-config-node": "^6.0.0", 59 | "ava": "^4.0.0", 60 | "c8": "^7.11.0", 61 | "esbuild": "^0.24.0", 62 | "husky": "^8.0.0" 63 | }, 64 | "engines": { 65 | "node": ">=16.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>netlify/renovate-config:esm'], 3 | ignorePresets: [':prHourlyLimit2'], 4 | semanticCommits: true, 5 | dependencyDashboard: true, 6 | automerge: true, 7 | packageRules: [], 8 | } 9 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import { buildSync } from 'esbuild' 2 | 3 | const jsBanner = ` 4 | import { createRequire as ___internalCreateRequire } from 'module'; 5 | import { fileURLToPath as ___internalFileURLToPath } from "url"; 6 | import { dirname as ___internalPathDirname} from "path"; 7 | let __filename =___internalFileURLToPath(import.meta.url); 8 | let __dirname = ___internalPathDirname(___internalFileURLToPath(import.meta.url)); 9 | let require = ___internalCreateRequire(import.meta.url); 10 | ` 11 | 12 | buildSync({ 13 | banner: { 14 | js: jsBanner, 15 | }, 16 | bundle: true, 17 | entryPoints: ['src/index.js'], 18 | format: 'esm', 19 | platform: 'node', 20 | outdir: 'dist', 21 | sourcemap: true, 22 | }) 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import core from '@actions/core' 2 | import github from '@actions/github' 3 | 4 | import { 5 | createHeadBranchComment, 6 | createPullRequestComment, 7 | findDeltaComment, 8 | getMetricsComment, 9 | } from './lib/comment.js' 10 | import { readDeltaFiles } from './lib/delta_file.js' 11 | import { getCommentsFromMainBranch } from './lib/github.js' 12 | import { getInputs } from './lib/inputs.js' 13 | 14 | const processHeadBranch = async ({ commitSha, headMetrics, job, octokit, owner, repo, title }) => { 15 | const previousCommit = await getCommentsFromMainBranch({ commitIndex: 1, octokit, owner, repo }) 16 | const comment = createHeadBranchComment({ commitSha, job, metrics: headMetrics, previousCommit, title }) 17 | 18 | await octokit.rest.repos.createCommitComment({ 19 | owner, 20 | repo, 21 | commit_sha: commitSha, 22 | body: comment, 23 | }) 24 | } 25 | 26 | const processPullRequest = async ({ headMetrics, job, octokit, owner, prNumber, repo, title, style }) => { 27 | const { baseSha, comments } = await getCommentsFromMainBranch({ octokit, owner, repo }) 28 | const baseMetrics = getMetricsComment({ comments, job }) 29 | 30 | core.debug(`Base metrics: ${JSON.stringify(baseMetrics)}`) 31 | 32 | const comment = createPullRequestComment({ 33 | baseSha, 34 | job, 35 | metrics: headMetrics, 36 | previousMetrics: baseMetrics, 37 | title, 38 | style, 39 | }) 40 | const existingComments = await octokit.paginate(octokit.rest.issues.listComments, { 41 | owner, 42 | repo, 43 | issue_number: prNumber, 44 | }) 45 | const existingDeltaComment = existingComments.find((existingComment) => findDeltaComment(existingComment.body, job)) 46 | 47 | if (existingDeltaComment) { 48 | core.debug(`Updating existing delta comment: ${existingDeltaComment.url}`) 49 | 50 | await octokit.rest.issues.updateComment({ 51 | owner, 52 | repo, 53 | comment_id: existingDeltaComment.id, 54 | body: comment, 55 | }) 56 | } else { 57 | core.debug('Creating new delta comment') 58 | 59 | await octokit.rest.issues.createComment({ 60 | owner, 61 | repo, 62 | issue_number: prNumber, 63 | body: comment, 64 | }) 65 | } 66 | } 67 | 68 | const run = async function () { 69 | const { baseBranch, commitSha, job, owner, prNumber, ref, repo, rootPath, title, style, token } = getInputs() 70 | const headMetrics = await readDeltaFiles(rootPath) 71 | 72 | core.debug(`Running job ${job} on ref ${ref}`) 73 | 74 | if (headMetrics.length === 0) { 75 | core.debug(`No metrics found`) 76 | 77 | return 78 | } 79 | 80 | core.debug(`Found metrics: ${JSON.stringify(headMetrics)}`) 81 | 82 | const octokit = github.getOctokit(token) 83 | const isPR = Boolean(prNumber) 84 | 85 | if (!isPR && ref === `refs/heads/${baseBranch}`) { 86 | core.debug(`This run is related to the ${baseBranch} branch`) 87 | 88 | await processHeadBranch({ commitSha, headMetrics, job, octokit, owner, repo, title }) 89 | } else if (isPR) { 90 | core.debug(`This run is related to PR #${prNumber}`) 91 | 92 | await processPullRequest({ headMetrics, job, octokit, owner, prNumber, repo, title, style }) 93 | } else { 94 | core.debug(`This run is not related to a PR or the default branch`) 95 | } 96 | } 97 | 98 | try { 99 | run() 100 | } catch (error) { 101 | core.debug(`Error: ${JSON.stringify(error)}`) 102 | core.setFailed(error.message) 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/comment.js: -------------------------------------------------------------------------------- 1 | import regexEscape from 'regex-escape' 2 | 3 | import { drawGraph } from './graph.js' 4 | import { formatValue } from './units.js' 5 | 6 | const MAX_GRAPH_ITEMS = 13 7 | const PAST_METRICS_COUNT = 30 8 | 9 | export const createHeadBranchComment = ({ commitSha, metrics, job, previousCommit, title }) => { 10 | const allMetrics = getMetricsForHeadBranch({ commitSha, job, metrics, previousCommit }) 11 | const metadata = `` 12 | const metricsList = metrics.map((metric) => getMetricLine(metric)).join('\n') 13 | 14 | return `## ${title}\n\n${metricsList}\n${metadata}` 15 | } 16 | 17 | export const createPullRequestComment = ({ baseSha, job, metrics, previousMetrics, title, style }) => { 18 | // Accounting for both the legacy metadata format (object) and the new 19 | // format (array of objects). 20 | const previousMetricsArray = (Array.isArray(previousMetrics) ? previousMetrics : [previousMetrics]) 21 | .filter(Boolean) 22 | .reverse() 23 | const metadata = `` 24 | const metricsList = metrics 25 | .map((metric) => { 26 | const comparison = previousMetricsArray.at(-1) ?? {} 27 | const previousValue = comparison[metric.name] 28 | // eslint-disable-next-line dot-notation 29 | const previousSha = comparison['__commit'] 30 | if (style === 'graph') { 31 | const graphMetrics = [...previousMetricsArray, { __commit: baseSha, [metric.name]: metric.value }] 32 | const graph = getGraph({ metrics: graphMetrics, metricName: metric.name, units: metric.units }) 33 | return getMetricLineWithGraph(metric, previousValue, previousSha, graph) 34 | } 35 | return getMetricLine(metric, previousValue, previousSha) 36 | }) 37 | .join('\n') 38 | const baseShaLine = baseSha && previousMetricsArray.length !== 0 ? `*Comparing with ${baseSha}*\n\n` : '' 39 | 40 | return `## ${title}\n\n${baseShaLine}${metricsList}\n${metadata}` 41 | } 42 | 43 | const getGraph = ({ metrics, metricName, units }) => { 44 | const points = metrics.map((metric, index) => { 45 | const offset = metrics.length - 1 - index 46 | const label = offset === 0 ? 'T' : `T-${offset}` 47 | 48 | return { 49 | // eslint-disable-next-line dot-notation 50 | commit: metric['__commit'], 51 | displayValue: metric[metricName] == null ? 'n/a' : formatValue(metric[metricName], units), 52 | label, 53 | value: metric[metricName] == null ? Number.NEGATIVE_INFINITY : metric[metricName], 54 | } 55 | }) 56 | const graph = drawGraph(points.slice(MAX_GRAPH_ITEMS * -1), { drawMean: true, fillLast: true }) 57 | const legendItems = points 58 | .map( 59 | ({ commit, displayValue, label }) => 60 | `- ${label === 'T' ? '**' : ''}${label} (${label === 'T' ? 'current commit' : commit}): ${displayValue}${ 61 | label === 'T' ? '**' : '' 62 | }`, 63 | ) 64 | .join('\n') 65 | const legend = `
\nLegend\n\n${legendItems}
` 66 | const lines = ['```', graph, '```', legend] 67 | 68 | return lines.join('\n') 69 | } 70 | 71 | export const getMetricsComment = ({ comments, job }) => { 72 | const deltaComment = comments.map(({ body }) => parseComment(body, job)).find(Boolean) 73 | 74 | return deltaComment 75 | } 76 | 77 | const getMetricsForHeadBranch = ({ commitSha, job, metrics, previousCommit }) => { 78 | const metricValues = metrics.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {}) 79 | const currentCommitMetrics = { __commit: commitSha, ...metricValues } 80 | 81 | if (previousCommit) { 82 | const previousMetrics = getMetricsComment({ comments: previousCommit.comments, job }) 83 | const normalizedPreviousMetrics = normalizeMetrics(previousMetrics, previousCommit.baseSha).slice( 84 | 0, 85 | PAST_METRICS_COUNT - 1, 86 | ) 87 | 88 | return [currentCommitMetrics, ...normalizedPreviousMetrics] 89 | } 90 | 91 | return [currentCommitMetrics] 92 | } 93 | 94 | const getMetricLine = ({ displayName, name, units, value }, previousValue, previousSha) => { 95 | const comparison = getMetricLineComparison(value, previousValue, previousSha) 96 | const formattedValue = formatValue(value, units) 97 | return `- **${displayName || name}**: ${formattedValue}${comparison ? ` ${comparison}` : ''}` 98 | } 99 | 100 | const getMetricLineWithGraph = ({ displayName, name, units, value }, previousValue, previousSha, graph = '') => { 101 | const comparison = getMetricLineComparison(value, previousValue, previousSha) 102 | const formattedValue = formatValue(value, units) 103 | return `### ${displayName || name}: ${formattedValue}\n${comparison ? ` ${comparison}` : ''}\n${graph}` 104 | } 105 | 106 | const getMetricLineComparison = (value, previousValue, previousSha) => { 107 | if (previousValue === undefined) { 108 | return '' 109 | } 110 | 111 | const difference = value - previousValue 112 | 113 | if (difference === 0) { 114 | return '(no change)' 115 | } 116 | 117 | const percentage = Math.abs((difference / value) * FLOAT_TO_PERCENT).toFixed(2) 118 | const [word, icon] = difference > 0 ? ['increase', '⬆️'] : ['decrease', '⬇️'] 119 | const shaText = previousSha ? ` vs. ${previousSha}` : '' 120 | 121 | return `${icon} **${percentage}% ${word}**${shaText}` 122 | } 123 | 124 | const FLOAT_TO_PERCENT = 100 125 | 126 | export const findDeltaComment = (body, job) => { 127 | const regex = new RegExp(``) 128 | const match = body.match(regex) 129 | 130 | return match 131 | } 132 | 133 | const normalizeMetrics = (metrics, sha) => { 134 | if (!metrics) { 135 | return [] 136 | } 137 | 138 | if (Array.isArray(metrics)) { 139 | return metrics 140 | } 141 | 142 | return [{ __commit: sha, ...metrics }] 143 | } 144 | 145 | const parseComment = (body, job) => { 146 | const match = findDeltaComment(body, job) 147 | 148 | if (!match) { 149 | return 150 | } 151 | 152 | try { 153 | return JSON.parse(match[1]) 154 | } catch { 155 | // no-op 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/lib/delta_file.js: -------------------------------------------------------------------------------- 1 | import { readdir, readFile } from 'fs' 2 | import { promisify } from 'util' 3 | 4 | import core from '@actions/core' 5 | 6 | const pReadDir = promisify(readdir) 7 | const pReadFile = promisify(readFile) 8 | 9 | const readDeltaFile = async (filePath) => { 10 | try { 11 | const data = await pReadFile(filePath, 'utf8') 12 | const match = data.match(/(\d+(?:\.\d+)?)\s*(\w*)\s*(?:\(([\s\S]*)\))?/) 13 | 14 | if (!match) { 15 | return null 16 | } 17 | 18 | return { 19 | value: Number(match[1]), 20 | units: match[2], 21 | displayName: match[3], 22 | } 23 | } catch { 24 | return null 25 | } 26 | } 27 | 28 | export const readDeltaFiles = async (rootPath) => { 29 | try { 30 | const items = await pReadDir(rootPath) 31 | const metricFiles = items 32 | .map((fileName) => ({ fileName, metricMatch: fileName.match(/^\.delta\.(.+)$/) })) 33 | .filter(({ metricMatch }) => Boolean(metricMatch)) 34 | .map(({ fileName, metricMatch }) => ({ fileName, metricName: metricMatch[1] })) 35 | const metrics = metricFiles.map(async ({ fileName, metricName }) => { 36 | const data = await readDeltaFile(fileName) 37 | 38 | if (!data) { 39 | return 40 | } 41 | 42 | return { 43 | ...data, 44 | name: metricName, 45 | } 46 | }) 47 | const metricsData = await Promise.all(metrics) 48 | 49 | return metricsData.filter(Boolean) 50 | } catch (error) { 51 | core.debug(`Could not read delta files: ${error.message}`) 52 | 53 | return [] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/github.js: -------------------------------------------------------------------------------- 1 | export const getBranchNameFromRef = (ref) => { 2 | const match = ref.match(/^refs\/heads\/(.*)$/) 3 | 4 | if (match) { 5 | return match[1] 6 | } 7 | } 8 | 9 | export const getCommentsFromMainBranch = async ({ commitIndex = 0, octokit, owner, repo }) => { 10 | const { data: commits } = await octokit.rest.repos.listCommits({ 11 | owner, 12 | repo, 13 | }) 14 | const baseSha = commits[commitIndex].sha 15 | const { data: comments } = await octokit.rest.repos.listCommentsForCommit({ 16 | owner, 17 | repo, 18 | commit_sha: baseSha, 19 | }) 20 | 21 | return { baseSha, comments } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/graph.js: -------------------------------------------------------------------------------- 1 | const BAR_BODY = '| |' 2 | const BAR_BODY_FILLED = '|▒▒|' 3 | const BAR_BODY_MEAN = '┼──┼' 4 | const BAR_TOP = '┌──┐' 5 | const GAP = ' ' 6 | const GAP_MEAN = '─' 7 | const GAP_LENGTH = 4 8 | const LINE_COUNT = 20 9 | 10 | const drawBase = (points) => { 11 | const axisPadding = '─'.repeat(GAP_LENGTH / 2) 12 | const axis = points 13 | .map(({ value }) => { 14 | const barChar = value === 0 ? '─' : '┴' 15 | 16 | return `${axisPadding}${barChar}${'─'.repeat(BAR_BODY.length - 2)}${barChar}${axisPadding}` 17 | }) 18 | .join('') 19 | const legend = points 20 | .map(({ label }) => { 21 | const text = getPaddedString(label, BAR_BODY.length + GAP_LENGTH) 22 | 23 | return text 24 | }) 25 | .join('') 26 | 27 | return `└─${axis}>\n ${legend}` 28 | } 29 | 30 | export const drawGraph = (values, { drawMean = false, fillLast = false } = {}) => { 31 | const maxValue = values.reduce((max, { value }) => (value > max ? value : max), 0) 32 | const usableValues = values.filter(({ value }) => value !== Number.NEGATIVE_INFINITY) 33 | const sum = usableValues.reduce((acc, { value }) => acc + value, 0) 34 | const mean = sum / usableValues.length 35 | const increment = maxValue / LINE_COUNT 36 | const meanLevel = drawMean ? LINE_COUNT - Math.round(mean / increment) : null 37 | const augmentedValues = values.map((dataPoint) => { 38 | const filledLevels = dataPoint.value === Number.NEGATIVE_INFINITY ? 0 : Math.round(dataPoint.value / increment) 39 | const filterValue = dataPoint.value === Number.NEGATIVE_INFINITY ? 0 : dataPoint.value 40 | 41 | return { ...dataPoint, value: filterValue, emptyLevels: LINE_COUNT - filledLevels } 42 | }) 43 | 44 | const levels = Array.from({ length: LINE_COUNT }, (_, index) => 45 | drawLevel({ fillLast, isMean: meanLevel === index + 1, level: index + 1, values: augmentedValues }), 46 | ) 47 | const topLevels = [ 48 | drawLevel({ level: -1, values: augmentedValues }), 49 | drawLevel({ level: 0, values: augmentedValues }), 50 | ] 51 | const base = drawBase(augmentedValues) 52 | 53 | return `${[...topLevels, ...levels].join('\n')}\n${base}` 54 | } 55 | 56 | const drawLevel = ({ fillLast, isMean, level, values }) => { 57 | const gapCharacter = isMean ? GAP_MEAN : GAP 58 | const padding = gapCharacter.repeat(GAP_LENGTH / 2) 59 | const bars = values 60 | // eslint-disable-next-line complexity 61 | .map(({ displayValue, emptyLevels, value }, index) => { 62 | const isLastValue = index === values.length - 1 63 | 64 | if (emptyLevels < level) { 65 | const unfilledBody = isMean ? BAR_BODY_MEAN : BAR_BODY 66 | 67 | return `${padding}${isLastValue && fillLast ? BAR_BODY_FILLED : unfilledBody}${padding}` 68 | } 69 | 70 | if (emptyLevels === level && value !== 0) { 71 | return `${padding}${BAR_TOP}${padding}` 72 | } 73 | 74 | if ((emptyLevels - 1 === level && value !== 0) || (emptyLevels === level && value === 0)) { 75 | return getPaddedString((displayValue || value).toString(), BAR_BODY.length + GAP_LENGTH, gapCharacter) 76 | } 77 | 78 | return `${padding}${gapCharacter.repeat(BAR_BODY.length)}${padding}` 79 | }) 80 | .join('') 81 | 82 | return `${level === -1 ? '^' : '│'} ${bars}` 83 | } 84 | 85 | const getPaddedString = (string, length, paddingCharacter = ' ') => { 86 | const totalPadding = length - string.length 87 | 88 | if (totalPadding < 0) { 89 | return `${string.slice(0, length - 1)}…` 90 | } 91 | 92 | const paddingRightLength = Math.max(0, Math.round(totalPadding / 2)) 93 | const paddingLeftLength = Math.max(0, totalPadding - paddingRightLength) 94 | const paddingLeft = paddingCharacter.repeat(paddingLeftLength) 95 | const paddingRight = paddingCharacter.repeat(paddingRightLength) 96 | 97 | return `${paddingLeft}${string}${paddingRight}` 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/inputs.js: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | import core from '@actions/core' 4 | import { context } from '@actions/github' 5 | 6 | const getPrNumber = () => { 7 | const { eventName, payload } = context 8 | 9 | if (eventName === 'pull_request') { 10 | return payload.number 11 | } 12 | 13 | const prNumberInput = Number.parseInt(core.getInput('pr_number')) 14 | if (Number.isInteger(prNumberInput)) { 15 | return prNumberInput 16 | } 17 | } 18 | 19 | export const getInputs = () => { 20 | const { 21 | GITHUB_DEV_BASE_BRANCH: envBaseBranch, 22 | GITHUB_REPOSITORY: repository, 23 | GITHUB_WORKSPACE: rootPath = process.cwd(), 24 | } = process.env 25 | const { job, ref, sha: commitSha } = context 26 | const baseBranch = envBaseBranch || core.getInput('base_branch') 27 | const title = core.getInput('title', { required: true }) 28 | const style = core.getInput('style', { required: false }) || 'graph' 29 | const [owner, repo] = repository.split('/') 30 | const token = core.getInput('token', { required: true }) 31 | const prNumber = getPrNumber() 32 | 33 | return { 34 | baseBranch, 35 | commitSha, 36 | job, 37 | owner, 38 | prNumber, 39 | ref, 40 | repo, 41 | rootPath, 42 | title, 43 | style, 44 | token, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/units.js: -------------------------------------------------------------------------------- 1 | import prettyBytes from 'pretty-bytes' 2 | import prettyMilliseconds from 'pretty-ms' 3 | 4 | const KILO = 1e3 5 | 6 | // eslint-disable-next-line complexity 7 | export const formatValue = (value, unit) => { 8 | const normalizedUnit = unit && unit.toLowerCase() 9 | 10 | switch (normalizedUnit) { 11 | case 'b': 12 | case 'byte': 13 | case 'bytes': 14 | return prettyBytes(value) 15 | 16 | case 'kb': 17 | case 'kilobyte': 18 | case 'kilobytes': 19 | return prettyBytes(value * KILO) 20 | 21 | case 'ms': 22 | case 'millisecond': 23 | case 'milliseconds': 24 | return prettyMilliseconds(value) 25 | 26 | case 's': 27 | case 'seconds': 28 | return prettyMilliseconds(value * KILO) 29 | 30 | default: 31 | return value.toLocaleString() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/lib/comment.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { createPullRequestComment } from '../../../src/lib/comment.js' 4 | 5 | test('createPullRequestComment ignores missing metrics', (t) => { 6 | const baseSha = '0d22eac17bedb89df6db003fe0f19b5cdb4062cc' 7 | /* eslint-disable no-magic-numbers */ 8 | const metrics = [ 9 | { value: 33_254.267_163_666_664, units: 'ms', name: 'largeDepsNft' }, 10 | { value: 50_735.233_039_666_666, units: 'ms', name: 'largeDepsZisi' }, 11 | ] 12 | const previousMetrics = [ 13 | { 14 | __commit: baseSha, 15 | largeDepsNft: 30_445.912_780_333_33, 16 | largeDepsZisi: 46_321.186_879_999_994, 17 | }, 18 | { 19 | __commit: '846798a00e801a7e936fca4226f84900e67df587', 20 | largeDepsNft: 35_916.193_093_666_654, 21 | }, 22 | { 23 | __commit: '433569875b474d701a748b41fc8146d626a2fef5', 24 | largeDepsNft: 39_139.893_285_333_33, 25 | largeDepsZisi: 57_660.846_667, 26 | }, 27 | ] 28 | /* eslint-enable no-magic-numbers */ 29 | 30 | t.snapshot(createPullRequestComment({ baseSha, metrics, previousMetrics, title: '', job: '', style: 'graph' })) 31 | }) 32 | 33 | 34 | test('createPullRequestComment supports text style', (t) => { 35 | const baseSha = '0d22eac17bedb89df6db003fe0f19b5cdb4062cc' 36 | /* eslint-disable no-magic-numbers */ 37 | const metrics = [ 38 | { value: 33_254.267_163_666_664, units: 'ms', name: 'largeDepsNft' }, 39 | { value: 50_735.233_039_666_666, units: 'ms', name: 'largeDepsZisi' }, 40 | ] 41 | const previousMetrics = [ 42 | { 43 | __commit: baseSha, 44 | largeDepsNft: 30_445.912_780_333_33, 45 | largeDepsZisi: 46_321.186_879_999_994, 46 | }, 47 | { 48 | __commit: '846798a00e801a7e936fca4226f84900e67df587', 49 | largeDepsNft: 35_916.193_093_666_654, 50 | }, 51 | { 52 | __commit: '433569875b474d701a748b41fc8146d626a2fef5', 53 | largeDepsNft: 39_139.893_285_333_33, 54 | largeDepsZisi: 57_660.846_667, 55 | }, 56 | ] 57 | /* eslint-enable no-magic-numbers */ 58 | 59 | t.snapshot(createPullRequestComment({ baseSha, metrics, previousMetrics, title: '', job: '', style: 'text' })) 60 | }) 61 | -------------------------------------------------------------------------------- /test/unit/lib/snapshots/comment.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/unit/lib/comment.js` 2 | 3 | The actual snapshot is saved in `comment.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## createPullRequestComment ignores missing metrics 8 | 9 | > Snapshot 1 10 | 11 | `## ␊ 12 | ␊ 13 | *Comparing with 0d22eac17bedb89df6db003fe0f19b5cdb4062cc*␊ 14 | ␊ 15 | ### largeDepsNft: 33.2s␊ 16 | ⬆️ **8.45% increase** vs. 0d22eac17bedb89df6db003fe0f19b5cdb4062cc␊ 17 | \`\`\`␊ 18 | ^ 39.1s ␊ 19 | │ ┌──┐ ␊ 20 | │ | | 35.9s ␊ 21 | │ ──┼──┼────┌──┐───────────33.2s──␊ 22 | │ | | | | 30.4s ┌──┐ ␊ 23 | │ | | | | ┌──┐ |▒▒| ␊ 24 | │ | | | | | | |▒▒| ␊ 25 | │ | | | | | | |▒▒| ␊ 26 | │ | | | | | | |▒▒| ␊ 27 | │ | | | | | | |▒▒| ␊ 28 | │ | | | | | | |▒▒| ␊ 29 | │ | | | | | | |▒▒| ␊ 30 | │ | | | | | | |▒▒| ␊ 31 | │ | | | | | | |▒▒| ␊ 32 | │ | | | | | | |▒▒| ␊ 33 | │ | | | | | | |▒▒| ␊ 34 | │ | | | | | | |▒▒| ␊ 35 | │ | | | | | | |▒▒| ␊ 36 | │ | | | | | | |▒▒| ␊ 37 | │ | | | | | | |▒▒| ␊ 38 | │ | | | | | | |▒▒| ␊ 39 | │ | | | | | | |▒▒| ␊ 40 | └───┴──┴────┴──┴────┴──┴────┴──┴──>␊ 41 | T-3 T-2 T-1 T ␊ 42 | \`\`\`␊ 43 |
␊ 44 | Legend␊ 45 | ␊ 46 | - T-3 (433569875b474d701a748b41fc8146d626a2fef5): 39.1s␊ 47 | - T-2 (846798a00e801a7e936fca4226f84900e67df587): 35.9s␊ 48 | - T-1 (0d22eac17bedb89df6db003fe0f19b5cdb4062cc): 30.4s␊ 49 | - **T (current commit): 33.2s**
␊ 50 | ### largeDepsZisi: 50.7s␊ 51 | ⬆️ **8.70% increase** vs. 0d22eac17bedb89df6db003fe0f19b5cdb4062cc␊ 52 | \`\`\`␊ 53 | ^ 57.6s ␊ 54 | │ ┌──┐ ␊ 55 | │ | | 50.7s ␊ 56 | │ ──┼──┼────────────────────┌──┐──␊ 57 | │ | | 46.3s |▒▒| ␊ 58 | │ | | ┌──┐ |▒▒| ␊ 59 | │ | | | | |▒▒| ␊ 60 | │ | | | | |▒▒| ␊ 61 | │ | | | | |▒▒| ␊ 62 | │ | | | | |▒▒| ␊ 63 | │ | | | | |▒▒| ␊ 64 | │ | | | | |▒▒| ␊ 65 | │ | | | | |▒▒| ␊ 66 | │ | | | | |▒▒| ␊ 67 | │ | | | | |▒▒| ␊ 68 | │ | | | | |▒▒| ␊ 69 | │ | | | | |▒▒| ␊ 70 | │ | | | | |▒▒| ␊ 71 | │ | | | | |▒▒| ␊ 72 | │ | | | | |▒▒| ␊ 73 | │ | | | | |▒▒| ␊ 74 | │ | | n/a | | |▒▒| ␊ 75 | └───┴──┴────────────┴──┴────┴──┴──>␊ 76 | T-3 T-2 T-1 T ␊ 77 | \`\`\`␊ 78 |
␊ 79 | Legend␊ 80 | ␊ 81 | - T-3 (433569875b474d701a748b41fc8146d626a2fef5): 57.6s␊ 82 | - T-2 (846798a00e801a7e936fca4226f84900e67df587): n/a␊ 83 | - T-1 (0d22eac17bedb89df6db003fe0f19b5cdb4062cc): 46.3s␊ 84 | - **T (current commit): 50.7s**
␊ 85 | ` 86 | 87 | ## createPullRequestComment supports text style 88 | 89 | > Snapshot 1 90 | 91 | `## ␊ 92 | ␊ 93 | *Comparing with 0d22eac17bedb89df6db003fe0f19b5cdb4062cc*␊ 94 | ␊ 95 | - **largeDepsNft**: 33.2s ⬆️ **8.45% increase** vs. 0d22eac17bedb89df6db003fe0f19b5cdb4062cc␊ 96 | - **largeDepsZisi**: 50.7s ⬆️ **8.70% increase** vs. 0d22eac17bedb89df6db003fe0f19b5cdb4062cc␊ 97 | ` 98 | -------------------------------------------------------------------------------- /test/unit/lib/snapshots/comment.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/delta-action/4bb88f7ffa9cf7f3c5c6e3271c3659431427806c/test/unit/lib/snapshots/comment.js.snap --------------------------------------------------------------------------------