├── .eslintrc.yml ├── .github └── workflows │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .npmignore ├── .release-it.cjs ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── github-changelog-generator.js ├── jest.config.js ├── package.json ├── src ├── changelog-fetcher.js ├── changelog-formatter.js └── index.js ├── test ├── changelog-fetcher.test.js ├── changelog-formatter.test.js └── setup.js └── yarn.lock /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: uphold 2 | 3 | rules: 4 | id-match: 5 | - error 6 | - '^_$|^[\$_a-zA-Z]*[_a-zA-Z0-9]*[a-zA-Z0-9]*$|^[A-Z][_A-Z0-9]+[A-Z0-9]$' 7 | - onlyDeclarations: true 8 | properties: true 9 | no-process-env: 0 10 | no-process-exit: 0 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | VERSION_BUMP: 7 | description: 'The version bump' 8 | type: choice 9 | options: 10 | - major 11 | - minor 12 | - patch 13 | default: minor 14 | required: true 15 | 16 | jobs: 17 | release: 18 | runs-on: ubuntu-latest 19 | environment: release 20 | concurrency: 1 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | with: 26 | persist-credentials: false 27 | 28 | - name: Setup Node.js version 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 22 32 | 33 | - name: Configure npm 34 | run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.RELEASE_NPM_TOKEN }} 35 | 36 | - name: Install dependencies 37 | run: | 38 | npm install -g yarn@1 39 | yarn install --frozen-lockfile 40 | 41 | - name: Configure git 42 | run: | 43 | git config user.name "Uphold" 44 | git config user.email "bot@uphold.com" 45 | git config --global url.https://${{ secrets.RELEASE_GITHUB_TOKEN }}@github.com/.insteadOf https://github.com/ 46 | 47 | - name: Generate release 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} 50 | NPM_TOKEN: ${{ secrets.RELEASE_NPM_TOKEN }} 51 | run: npm run release -- --increment "${{ github.event.inputs.VERSION_BUMP }}" -V 52 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | unit: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node: [ '20', '22' ] 11 | 12 | name: Node ${{ matrix.node }} 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: yarn install --frozen-lockfile 20 | - run: yarn lint 21 | - run: yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | coverage/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !bin/* 3 | !src/* 4 | -------------------------------------------------------------------------------- /.release-it.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | git: { 3 | changelog: 'echo "## Changelog\\n\\n$(bin/github-changelog-generator.js -f unreleased | tail -n +4 -f)"', 4 | commitMessage: 'Release ${version}', 5 | requireBranch: 'master', 6 | requireCommits: true, 7 | tagName: 'v${version}' 8 | }, 9 | github: { 10 | release: true, 11 | releaseName: 'v${version}' 12 | }, 13 | hooks: { 14 | 'after:bump': ` 15 | echo "$(bin/github-changelog-generator.js -f v\${version})\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md && 16 | git add CHANGELOG.md --all 17 | ` 18 | }, 19 | npm: { 20 | publish: true 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v4.0.2](https://github.com/uphold/github-changelog-generator/releases/tag/v4.0.2) (2025-03-21) 4 | 5 | - Switch release-it config to CommonJS [\#131](https://github.com/uphold/github-changelog-generator/pull/131) ([Americas](https://github.com/Americas)) 6 | 7 | ## [v4.0.1](https://github.com/uphold/github-changelog-generator/releases/tag/4.0.1) (2025-03-20) 8 | 9 | - Update release process configuration [\#130](https://github.com/uphold/github-changelog-generator/pull/130) ([risantos](https://github.com/risantos)) 10 | - Update release-it file type and CHANGELOG [\#129](https://github.com/uphold/github-changelog-generator/pull/129) ([risantos](https://github.com/risantos)) 11 | 12 | ## [v4.0.0](https://github.com/uphold/github-changelog-generator/releases/tag/4.0.0) (2025-03-20) 13 | 14 | - Remove debug from release workflow [\#128](https://github.com/uphold/github-changelog-generator/pull/128) ([Americas](https://github.com/Americas)) 15 | - Debug release workflow [\#127](https://github.com/uphold/github-changelog-generator/pull/127) ([Americas](https://github.com/Americas)) 16 | - Fix `package.json` configuration [\#126](https://github.com/uphold/github-changelog-generator/pull/126) ([risantos](https://github.com/risantos)) 17 | - Add release-it as a dev dependency [\#125](https://github.com/uphold/github-changelog-generator/pull/125) ([Americas](https://github.com/Americas)) 18 | - Update release workflow [\#123](https://github.com/uphold/github-changelog-generator/pull/123) ([risantos](https://github.com/risantos)) 19 | - Update CHANGELOG format output [\#122](https://github.com/uphold/github-changelog-generator/pull/122) ([risantos](https://github.com/risantos)) 20 | - Bump dependencies to fix security vulnerabilities [\#121](https://github.com/uphold/github-changelog-generator/pull/121) ([risantos](https://github.com/risantos)) 21 | 22 | ## [v3.4.0](https://github.com/uphold/github-changelog-generator/releases/tag/v3.4.0) (2023-11-03) 23 | 24 | - Add support for mono-repositories [\#119](https://github.com/uphold/github-changelog-generator/pull/119) ([satazor](https://github.com/satazor)) 25 | 26 | ## [v3.3.1](https://github.com/uphold/github-changelog-generator/releases/tag/v3.3.1) (2022-12-20) 27 | 28 | - Update dev dependencies [\#113](https://github.com/uphold/github-changelog-generator/pull/113) ([goncalvesnelson](https://github.com/goncalvesnelson)) 29 | - Bump minimist from 1.2.5 to 1.2.6 [\#104](https://github.com/uphold/github-changelog-generator/pull/104) ([dependabot](https://github.com/apps/dependabot)) 30 | 31 | ## [v3.3.0](https://github.com/uphold/github-changelog-generator/releases/tag/v3.3.0) (2022-07-11) 32 | 33 | - Bump node-fetch from 2.6.6 to 2.6.7 [\#106](https://github.com/uphold/github-changelog-generator/pull/106) ([dependabot](https://github.com/apps/dependabot)) 34 | - Bump moment from 2.29.1 to 2.29.4 [\#110](https://github.com/uphold/github-changelog-generator/pull/110) ([dependabot](https://github.com/apps/dependabot)) 35 | 36 | ## [v3.2.0](https://github.com/uphold/github-changelog-generator/releases/tag/v3.2.0) (2022-06-12) 37 | 38 | - Support http remotes when detecting repo and owner [\#107](https://github.com/uphold/github-changelog-generator/pull/107) ([satazor](https://github.com/satazor)) 39 | 40 | ## [v3.1.0](https://github.com/uphold/github-changelog-generator/releases/tag/v3.1.0) (2022-05-26) 41 | 42 | - Fix generator to work on repos without releases [\#102](https://github.com/uphold/github-changelog-generator/pull/102) ([Americas](https://github.com/Americas)) 43 | 44 | ## [v3.0.0](https://github.com/uphold/github-changelog-generator/releases/tag/v3.0.0) (2021-11-08) 45 | 46 | - Bump dependencies [\#96](https://github.com/uphold/github-changelog-generator/pull/96) ([diogotorres97](https://github.com/diogotorres97)) 47 | 48 | ## [v2.0.0](https://github.com/uphold/github-changelog-generator/releases/tag/v2.0.0) (2021-04-15) 49 | 50 | - Simplify version grep in release script [\#89](https://github.com/uphold/github-changelog-generator/pull/89) ([Americas](https://github.com/Americas)) 51 | - Add release script [\#88](https://github.com/uphold/github-changelog-generator/pull/88) ([Americas](https://github.com/Americas)) 52 | - Bump packages [\#86](https://github.com/uphold/github-changelog-generator/pull/86) ([Americas](https://github.com/Americas)) 53 | - Improve Changelog generation performance [\#80](https://github.com/uphold/github-changelog-generator/pull/80) ([goncalvesnelson](https://github.com/goncalvesnelson)) 54 | - Release 1.0.2 [\#74](https://github.com/uphold/github-changelog-generator/pull/74) ([cristianooliveira](https://github.com/cristianooliveira)) 55 | 56 | ## [v1.0.2](https://github.com/uphold/github-changelog-generator/releases/tag/v1.0.2) (2019-11-13) 57 | 58 | - Update octokit/rest@16.34.1 [\#73](https://github.com/uphold/github-changelog-generator/pull/73) ([cristianooliveira](https://github.com/cristianooliveira)) 59 | 60 | ## [v1.0.1](https://github.com/uphold/github-changelog-generator/releases/tag/v1.0.1) (2019-10-11) 61 | 62 | - Fix missing files from npm [\#71](https://github.com/uphold/github-changelog-generator/pull/71) ([rplopes](https://github.com/rplopes)) 63 | 64 | ## [v1.0.0](https://github.com/uphold/github-changelog-generator/releases/tag/v1.0.0) (2019-10-11) 65 | 66 | - Update jest@24.9.0 [\#70](https://github.com/uphold/github-changelog-generator/pull/70) ([rplopes](https://github.com/rplopes)) 67 | - Remove Babel [\#69](https://github.com/uphold/github-changelog-generator/pull/69) ([rplopes](https://github.com/rplopes)) 68 | - Update dependencies [\#68](https://github.com/uphold/github-changelog-generator/pull/68) ([rplopes](https://github.com/rplopes)) 69 | - Upgrade dependencies [\#67](https://github.com/uphold/github-changelog-generator/pull/67) ([rplopes](https://github.com/rplopes)) 70 | 71 | ## [v0.8.1](https://github.com/uphold/github-changelog-generator/releases/tag/v0.8.1) (2019-02-27) 72 | 73 | - Update resolved dependencies [\#66](https://github.com/uphold/github-changelog-generator/pull/66) ([rplopes](https://github.com/rplopes)) 74 | - Update dependencies [\#65](https://github.com/uphold/github-changelog-generator/pull/65) ([rplopes](https://github.com/rplopes)) 75 | - Fix GitHub deprecation warnings [\#64](https://github.com/uphold/github-changelog-generator/pull/64) ([rplopes](https://github.com/rplopes)) 76 | 77 | ## [v0.8.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.8.0) (2018-11-28) 78 | 79 | - Add option to filter PRs by labels [\#62](https://github.com/uphold/github-changelog-generator/pull/62) ([Americas](https://github.com/Americas)) 80 | - Update dev dependencies and flow [\#61](https://github.com/uphold/github-changelog-generator/pull/61) ([Americas](https://github.com/Americas)) 81 | - Use shorthanded call to generate changelog [\#59](https://github.com/uphold/github-changelog-generator/pull/59) ([Americas](https://github.com/Americas)) 82 | - Add support for node engine >= 6 [\#58](https://github.com/uphold/github-changelog-generator/pull/58) ([Americas](https://github.com/Americas)) 83 | - Update github package to octokit [\#57](https://github.com/uphold/github-changelog-generator/pull/57) ([Americas](https://github.com/Americas)) 84 | - Extract components [\#54](https://github.com/uphold/github-changelog-generator/pull/54) ([rplopes](https://github.com/rplopes)) 85 | 86 | ## [v0.7.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.7.0) (2018-06-07) 87 | 88 | - Infer owner and repo from git config [\#50](https://github.com/uphold/github-changelog-generator/pull/50) ([rplopes](https://github.com/rplopes)) 89 | 90 | ## [v0.6.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.6.0) (2018-04-06) 91 | 92 | - Fix excessive API calls [\#49](https://github.com/uphold/github-changelog-generator/pull/49) ([rplopes](https://github.com/rplopes)) 93 | - Fix GitHub name [\#46](https://github.com/uphold/github-changelog-generator/pull/46) ([rplopes](https://github.com/rplopes)) 94 | 95 | ## [v0.5.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.5.0) (2018-03-02) 96 | 97 | - Generation of CHANGELOG filtering merges by base branch [\#43](https://github.com/uphold/github-changelog-generator/pull/43) ([marianacapelo](https://github.com/marianacapelo)) 98 | 99 | ## [v0.4.1](https://github.com/uphold/github-changelog-generator/releases/tag/v0.4.1) (2017-03-17) 100 | 101 | - Fix npm pack ignoring bin folder [\#41](https://github.com/uphold/github-changelog-generator/pull/41) ([kurayama](https://github.com/kurayama)) 102 | - Add repository in package.json [\#40](https://github.com/uphold/github-changelog-generator/pull/40) ([kurayama](https://github.com/kurayama)) 103 | 104 | ## [v0.4.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.4.0) (2017-03-17) 105 | 106 | - Use @uphold scope [\#37](https://github.com/uphold/github-changelog-generator/pull/37) ([kurayama](https://github.com/kurayama)) 107 | - Whitelist files in .npmignore [\#36](https://github.com/uphold/github-changelog-generator/pull/36) ([kurayama](https://github.com/kurayama)) 108 | - Fix empty release names [\#33](https://github.com/uphold/github-changelog-generator/pull/33) ([rplopes](https://github.com/rplopes)) 109 | - Fix typo in Usage section [\#34](https://github.com/uphold/github-changelog-generator/pull/34) ([hitmanmcc](https://github.com/hitmanmcc)) 110 | 111 | ## [v0.3.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.3.0) (2017-01-17) 112 | 113 | - Update changelog script [\#30](https://github.com/uphold/github-changelog-generator/pull/30) ([rplopes](https://github.com/rplopes)) 114 | - Allow specifying release tag names [\#29](https://github.com/uphold/github-changelog-generator/pull/29) ([rplopes](https://github.com/rplopes)) 115 | - Add command line interface arguments [\#28](https://github.com/uphold/github-changelog-generator/pull/28) ([Americas](https://github.com/Americas)) 116 | - Simplify babel usage [\#27](https://github.com/uphold/github-changelog-generator/pull/27) ([rplopes](https://github.com/rplopes)) 117 | 118 | ## [v0.2.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.2.0) (2016-11-25) 119 | 120 | - Transpile before running changelog [\#22](https://github.com/uphold/github-changelog-generator/pull/22) ([rplopes](https://github.com/rplopes)) 121 | - Fix executable path [\#21](https://github.com/uphold/github-changelog-generator/pull/21) ([kurayama](https://github.com/kurayama)) 122 | - Fix future release URL [\#20](https://github.com/uphold/github-changelog-generator/pull/20) ([rplopes](https://github.com/rplopes)) 123 | - Add scripts for generating changelog and version [\#9](https://github.com/uphold/github-changelog-generator/pull/9) ([rplopes](https://github.com/rplopes)) 124 | - Remove `dist/` from `.gitignore` [\#7](https://github.com/uphold/github-changelog-generator/pull/7) ([rplopes](https://github.com/rplopes)) 125 | - Fix Pull Request number display [\#6](https://github.com/uphold/github-changelog-generator/pull/6) ([rplopes](https://github.com/rplopes)) 126 | 127 | ## [v0.1.0](https://github.com/uphold/github-changelog-generator/releases/tag/v0.1.0) (2016-11-25) 128 | 129 | - Move babel-polyfill to dependency [\#5](https://github.com/uphold/github-changelog-generator/pull/5) ([kurayama](https://github.com/kurayama)) 130 | - Fix last page [\#4](https://github.com/uphold/github-changelog-generator/pull/4) ([kurayama](https://github.com/kurayama)) 131 | - Add .npmignore [\#3](https://github.com/uphold/github-changelog-generator/pull/3) ([kurayama](https://github.com/kurayama)) 132 | - Add yarn.lock [\#2](https://github.com/uphold/github-changelog-generator/pull/2) ([kurayama](https://github.com/kurayama)) 133 | - Add initial implementation [\#1](https://github.com/uphold/github-changelog-generator/pull/1) ([rplopes](https://github.com/rplopes)) 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Uphold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-changelog-generator 2 | 3 | Generate changelog files from the project's GitHub PRs. 4 | 5 | ## Usage 6 | 7 | Generate a new [GitHub Personal Access Token](https://github.com/settings/tokens) and save it to your `.zshrc.local`, `.bashrc.local` or similar: 8 | 9 | ```sh 10 | export GITHUB_TOKEN= 11 | ``` 12 | 13 | To see a list of available options, run the following command: 14 | 15 | ```sh 16 | $ github-changelog-generator --help 17 | 18 | Usage: github-changelog-generator [options] 19 | 20 | Run GitHub changelog generator. 21 | 22 | Options: 23 | 24 | -h, --help output usage information 25 | -b, --base-branch [optional] specify the base branch name - master by default 26 | -f, --future-release [optional] specify the next release version 27 | -t, --future-release-tag [optional] specify the next release tag name if it is different from the release version 28 | -rtp, --release-tag-prefix [optional] release tag prefix to consider when finding the latest release, useful for monorepos 29 | -cfp, --changed-files-prefix [optional] changed files prefix to consider when finding pull-requests, useful for monorepos 30 | -l, --labels [optional] labels to filter pull requests by 31 | -o, --owner [optional] owner of the repository 32 | -r, --repo [optional] name of the repository 33 | --rebuild rebuild the full changelog 34 | ``` 35 | 36 | To generate a changelog for your GitHub project, use the following command: 37 | 38 | ```sh 39 | $ echo "$(github-changelog-generator --base-branch= --future-release= --future-release-tag= --owner= --repo=)\n$(tail -n +2 )" > 40 | ``` 41 | 42 | The `--base-branch` option allows you to filter the PRs by base branch. If omitted, it will default to branch master. 43 | 44 | Example: 45 | 46 | ```sh 47 | $ echo "$(github-changelog-generator --base-branch=production)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md 48 | ``` 49 | 50 | The `--future-release` and `--future-release-tag` options are optional. If your future release tag name is the same as your future release version number, then you can skip `--future-release-tag`. 51 | 52 | Example: 53 | 54 | ```sh 55 | $ echo "$(github-changelog-generator --future-release=1.2.3 --future-release-tag=v1.2.3)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md 56 | ``` 57 | 58 | If you are on a mono-repository, you will need to use `--release-tag-prefix` in order to filter release tags of the package you are targeting. There's also the `--changed-files-prefix` option that may be used to specify the base directory of the package. However, this should be automatic in most cases, except when we are unable to infer the root folder. 59 | 60 | Example: 61 | 62 | ```sh 63 | $ echo "$(github-changelog-generator --future-release=my-package@1.2.3 --future-release-tag=my-package@v1.2.3 --release-tag-prefix=my-package@v)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md 64 | ``` 65 | 66 | The `--owner` and `--repo` options allow you to specify the owner and name of the GitHub repository, respectively. If omitted, they will default to the values found in the project's git config. 67 | 68 | Example: 69 | 70 | ```sh 71 | $ echo "$(github-changelog-generator --owner=uphold --repo=github-changelog-generator)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md 72 | ``` 73 | 74 | The `--labels` option allows you to filter what pull requests are used by their labels. This is useful for repositories with more than one project, by labeling each pull request by what project they belong to, generating a changelog for each project becomes as simple as: 75 | 76 | Example: 77 | 78 | ```sh 79 | $ echo "$(github-changelog-generator --labels projectX,general)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md 80 | ``` 81 | 82 | The `--rebuild` option allows you to fetch the repository's full changelog history. 83 | Starting on major version 2, the default behavior for the generator is to only create the changelog for the pull requests that come after the latest release, 84 | so this option allows for backwards compatibility. 85 | 86 | Example: 87 | 88 | ```sh 89 | $ github-changelog-generator --rebuild > CHANGELOG.md 90 | ``` 91 | 92 | ## Release 93 | 94 | The release process is automated via the [release](https://github.com/uphold/github-changelog-generator/actions/workflows/release.yaml) GitHub workflow. Run it by clicking the "Run workflow" button. 95 | -------------------------------------------------------------------------------- /bin/github-changelog-generator.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import '../src/index.js'; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | clearMocks: true, 3 | collectCoverage: true, 4 | collectCoverageFrom: ['src/**/*.js', '!src/index.js'], 5 | modulePaths: [''], 6 | restoreMocks: true, 7 | setupFilesAfterEnv: ['/test/setup.js'], 8 | testEnvironment: 'node', 9 | transform: {} 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uphold/github-changelog-generator", 3 | "version": "4.0.2", 4 | "description": "Generate changelog files from the project's GitHub PRs", 5 | "license": "MIT", 6 | "author": "Ricardo Lopes", 7 | "main": "src/index.js", 8 | "type": "module", 9 | "bin": { 10 | "github-changelog-generator": "bin/github-changelog-generator.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/uphold/github-changelog-generator.git" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "scripts": { 20 | "lint": "eslint --cache src test", 21 | "release": "release-it", 22 | "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest" 23 | }, 24 | "dependencies": { 25 | "@octokit/graphql": "^8.2.1", 26 | "commander": "^8.3.0", 27 | "ini": "^2.0.0", 28 | "look-it-up": "^2.1.0", 29 | "moment": "^2.29.1" 30 | }, 31 | "devDependencies": { 32 | "@fastify/pre-commit": "^2.2.0", 33 | "@types/node": "^22.13.10", 34 | "eslint": "^8.30.0", 35 | "eslint-config-uphold": "^6.0.0", 36 | "jest": "^29.3.1", 37 | "nock": "^14.0.1", 38 | "prettier": "^2.8.1", 39 | "release-it": "^18.1.2" 40 | }, 41 | "engines": { 42 | "node": ">=20" 43 | }, 44 | "pre-commit": { 45 | "run": [ 46 | "lint" 47 | ], 48 | "silent": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/changelog-fetcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import { graphql } from '@octokit/graphql'; 6 | import moment from 'moment'; 7 | 8 | /** 9 | * `ChangelogFetcher` class. 10 | */ 11 | 12 | class ChangelogFetcher { 13 | /** 14 | * Constructor. 15 | */ 16 | 17 | constructor({ 18 | base, 19 | changedFilesPrefix, 20 | futureRelease, 21 | futureReleaseTag, 22 | labels, 23 | owner, 24 | releaseTagPrefix, 25 | repo, 26 | token 27 | }) { 28 | this.base = base; 29 | this.changedFilesPrefix = changedFilesPrefix; 30 | this.futureRelease = futureRelease; 31 | this.futureReleaseTag = futureReleaseTag || futureRelease; 32 | this.releaseTagPrefix = releaseTagPrefix; 33 | this.labels = labels || []; 34 | this.owner = owner; 35 | this.repo = repo; 36 | this.client = graphql.defaults({ 37 | baseUrl: 'https://api.github.com', 38 | headers: { authorization: `token ${token}` } 39 | }); 40 | } 41 | 42 | /** 43 | * Fetch the full changelog. 44 | * 45 | * @return {Array} releases - An array of objects with information about the all releases 46 | */ 47 | async fetchFullChangelog() { 48 | // Get the date the repository was created. 49 | const repositoryCreatedAt = await this.getRepositoryCreatedAt(); 50 | 51 | // Get all releases. 52 | const releases = await this.getReleases(); 53 | 54 | // Get all pull requests. 55 | const pullRequests = await this.getPullRequestsStartingFrom(repositoryCreatedAt); 56 | 57 | let cursor = 0; 58 | 59 | for (const pullRequest of pullRequests) { 60 | const mergedAt = moment.utc(pullRequest.mergedAt); 61 | 62 | let isAfterNextRelease = !releases[cursor + 1] || mergedAt.isAfter(releases[cursor + 1].createdAt); 63 | 64 | while (!isAfterNextRelease) { 65 | cursor += 1; 66 | isAfterNextRelease = !releases[cursor + 1] || mergedAt.isAfter(releases[cursor + 1].createdAt); 67 | } 68 | 69 | const isBeforeCurrentRelease = mergedAt.isSameOrBefore(releases[cursor].createdAt); 70 | 71 | if (isBeforeCurrentRelease) { 72 | releases[cursor].pullRequests.push(pullRequest); 73 | } 74 | } 75 | 76 | return releases; 77 | } 78 | 79 | /** 80 | * Fetch changelog after the latest release. 81 | * 82 | * @return {Array} releases - An array with an object with information about the latest release 83 | */ 84 | async fetchLatestChangelog() { 85 | // Don't do anything if a futureRelease was not specified. 86 | if (!this.futureRelease) { 87 | return []; 88 | } 89 | 90 | // Get the latest release. 91 | const latestRelease = this.releaseTagPrefix 92 | ? await this.getLatestReleaseByTagPrefix() 93 | : await this.getLatestRelease(); 94 | 95 | if (latestRelease.tagName === this.futureReleaseTag) { 96 | throw new Error('Changelog already on the latest release'); 97 | } 98 | 99 | // Get PRs up to the last release commit date. 100 | const pullRequests = await this.getPullRequestsStartingFrom(latestRelease.tagCommit.committedDate); 101 | 102 | return [ 103 | { 104 | createdAt: moment.utc(), 105 | name: this.futureRelease, 106 | pullRequests, 107 | tagName: this.futureReleaseTag, 108 | url: `https://github.com/${this.owner}/${this.repo}/releases/tag/${this.futureReleaseTag}` 109 | } 110 | ]; 111 | } 112 | 113 | /** 114 | * Get the latest release. 115 | * 116 | * @return {Object} release - the latest release 117 | */ 118 | async getLatestRelease() { 119 | const query = ` 120 | query getLatestRelease($owner: String!, $repo: String!) { 121 | repository(owner: $owner, name: $repo){ 122 | latestRelease { 123 | name, 124 | tagCommit { 125 | committedDate 126 | } 127 | tagName 128 | url 129 | } 130 | createdAt 131 | } 132 | }`; 133 | const result = await this.client(query, { owner: this.owner, repo: this.repo }); 134 | 135 | // Extract release from nested result. 136 | const release = result.repository.latestRelease; 137 | 138 | // For shiny new repositories without releases, use the repository creation date instead. 139 | if (!release) { 140 | return { tagCommit: { committedDate: moment.utc(result.repository.createdAt) } }; 141 | } 142 | 143 | // Transform string timestamp into moment. 144 | release.tagCommit.committedDate = moment.utc(release.tagCommit.committedDate); 145 | 146 | return release; 147 | } 148 | 149 | /** 150 | * Get the latest release by tag prefix. 151 | * 152 | * @return {Object} release - the latest release 153 | */ 154 | async getLatestReleaseByTagPrefix() { 155 | let cursor = ''; 156 | let hasMoreResults = true; 157 | let releases = []; 158 | let matchingRelease; 159 | 160 | do { 161 | ({ cursor, hasMoreResults, releases } = await this.getReleasesQuery(cursor)); 162 | 163 | matchingRelease = releases[0]; 164 | } while (!matchingRelease && hasMoreResults); 165 | 166 | // For shiny new repositories without releases, use the repository creation date instead. 167 | if (!matchingRelease) { 168 | const repositoryCreatedAt = await this.getRepositoryCreatedAt(); 169 | 170 | return { tagCommit: { committedDate: repositoryCreatedAt } }; 171 | } 172 | 173 | // Transform string timestamp into moment. 174 | matchingRelease.tagCommit.committedDate = moment.utc(matchingRelease.tagCommit.committedDate); 175 | 176 | return matchingRelease; 177 | } 178 | 179 | /** 180 | * Auxiliary function to iterate through list of PRs. 181 | * 182 | * @param {String} cursor - the cursor from where the function will get the PRs 183 | * @param {Number} pageSize - the number of results we try to fetch each time 184 | * @return {Map} {cursor, hasMoreResults, pullRequests} - An array of maps with information about the last PRs 185 | * starting from cursor (from newest to oldest) 186 | */ 187 | async getPullRequestsQuery(cursor = '', pageSize = 30) { 188 | const [cursorSignature, cursorParam] = cursor ? [', $cursor: String!', ', after: $cursor'] : ['', '']; 189 | 190 | const [labelsSignature, labelsParam] = 191 | this.labels.length === 0 ? ['', ''] : [', $labels: [String!]', ', labels: $labels']; 192 | 193 | const filesFragment = ` 194 | files (first: 100) { 195 | nodes { 196 | path 197 | } 198 | }`; 199 | 200 | // TODO: replace orderBy from UPDATED_AT to MERGED_AT when available. 201 | const query = ` 202 | query getPullRequests($owner: String!, $repo: String!, $first: Int!, $base: String!${cursorSignature}${labelsSignature}) { 203 | repository(owner: $owner, name: $repo) { 204 | pullRequests(baseRefName: $base, first: $first, orderBy: {field: UPDATED_AT, direction: DESC}, states: [MERGED]${cursorParam}${labelsParam}) { 205 | nodes { 206 | mergedAt 207 | title 208 | number 209 | updatedAt 210 | url 211 | baseRefName 212 | author { 213 | login 214 | url 215 | } 216 | ${this.changedFilesPrefix ? filesFragment : ''} 217 | } 218 | pageInfo { 219 | endCursor 220 | hasNextPage 221 | } 222 | } 223 | } 224 | }`; 225 | const result = await this.client(query, { 226 | base: this.base, 227 | cursor, 228 | first: pageSize, 229 | labels: this.labels, 230 | owner: this.owner, 231 | repo: this.repo 232 | }); 233 | 234 | let pullRequests = result.repository.pullRequests.nodes; 235 | 236 | if (this.changedFilesPrefix) { 237 | pullRequests = pullRequests 238 | .filter(({ files }) => files.nodes.some(file => file.path.startsWith(this.changedFilesPrefix))) 239 | // eslint-disable-next-line no-unused-vars 240 | .map(({ files, ...rest }) => rest); 241 | } 242 | 243 | return { 244 | cursor: result.repository.pullRequests.pageInfo.endCursor, 245 | hasMoreResults: result.repository.pullRequests.pageInfo.hasNextPage, 246 | pullRequests 247 | }; 248 | } 249 | 250 | /** 251 | * Get the list of approved pull requests merged after a given timestamp. 252 | * 253 | * @param {moment} startDate - the timestamp of the release after which we want to retrieve the pull requests 254 | * @return {Array} pullRequests - An array of maps with information about the pull requests 255 | */ 256 | async getPullRequestsStartingFrom(startDate) { 257 | const result = []; 258 | let stop = false; 259 | let hasMoreResults = true; 260 | let cursor = ''; 261 | let pullRequests = []; 262 | 263 | while (!stop && hasMoreResults) { 264 | ({ cursor, hasMoreResults, pullRequests } = await this.getPullRequestsQuery(cursor)); 265 | 266 | for (let i = 0; i < pullRequests.length; i++) { 267 | // If PR was merged after the release timestamp, save it to the result. 268 | if (startDate.isBefore(pullRequests[i].mergedAt)) { 269 | result.push(pullRequests[i]); 270 | } else if (startDate.isAfter(pullRequests[i].updatedAt)) { 271 | // Stop the iteration. 272 | stop = true; 273 | break; 274 | } 275 | } 276 | } 277 | 278 | // eslint-disable-next-line id-length 279 | return result.sort((a, b) => moment(b.mergedAt).diff(a.mergedAt)); 280 | } 281 | 282 | /** 283 | * Auxiliary function to iterate through list of releases. 284 | * 285 | * @param {String} cursor - the cursor from where the function will get the releases 286 | * @param {Number} pageSize - the number of results we try to fetch each time 287 | * @return {Map} {cursor, hasMoreResults, releases} - An array of maps with information about the last releases 288 | * starting from cursor (from newest to oldest) 289 | */ 290 | async getReleasesQuery(cursor = '', pageSize = 30) { 291 | const [cursorSignature, cursorParam] = cursor ? [', $cursor: String!', ', after: $cursor'] : ['', '']; 292 | const query = ` 293 | query getReleases($owner: String!, $repo: String!, $first: Int!${cursorSignature}) { 294 | repository(owner: $owner, name: $repo) { 295 | releases(first: $first, orderBy: {field: CREATED_AT, direction: DESC}${cursorParam}) { 296 | nodes { 297 | name 298 | tagName 299 | tagCommit { 300 | committedDate 301 | } 302 | url 303 | } 304 | pageInfo { 305 | endCursor 306 | hasNextPage 307 | } 308 | } 309 | } 310 | }`; 311 | const result = await this.client(query, { 312 | cursor, 313 | first: pageSize, 314 | owner: this.owner, 315 | repo: this.repo 316 | }); 317 | 318 | let releases = result.repository.releases.nodes; 319 | 320 | if (this.releaseTagPrefix) { 321 | releases = releases.filter(({ tagName }) => tagName.startsWith(this.releaseTagPrefix)); 322 | } 323 | 324 | return { 325 | cursor: result.repository.releases.pageInfo.endCursor, 326 | hasMoreResults: result.repository.releases.pageInfo.hasNextPage, 327 | releases 328 | }; 329 | } 330 | 331 | /** 332 | * Get the list of all releases of the repository. 333 | * 334 | * @return {Array} releases - An array of objects with information about the releases 335 | */ 336 | async getReleases() { 337 | const result = []; 338 | let cursor = ''; 339 | let hasMoreResults = true; 340 | let releases = []; 341 | 342 | do { 343 | ({ cursor, hasMoreResults, releases } = await this.getReleasesQuery(cursor)); 344 | 345 | result.push( 346 | ...releases.map(({ name, tagCommit, tagName, url }) => ({ 347 | createdAt: moment(tagCommit.committedDate), 348 | name, 349 | pullRequests: [], 350 | tagName, 351 | url 352 | })) 353 | ); 354 | } while (hasMoreResults); 355 | 356 | return result; 357 | } 358 | 359 | /** 360 | * Get the date that the repository was created. 361 | * 362 | * @return {moment} createdAt - the date the repository was created 363 | */ 364 | async getRepositoryCreatedAt() { 365 | const query = ` 366 | query getRepositoryCreatedAt($owner: String!, $repo: String!) { 367 | repository(owner: $owner, name: $repo) { 368 | createdAt 369 | } 370 | }`; 371 | const result = await this.client(query, { owner: this.owner, repo: this.repo }); 372 | 373 | return moment.utc(result.repository.createdAt); 374 | } 375 | } 376 | 377 | /** 378 | * Export `ChangelogFetcher`. 379 | */ 380 | 381 | export default ChangelogFetcher; 382 | -------------------------------------------------------------------------------- /src/changelog-formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Export `formatChangelog`. 5 | */ 6 | 7 | export const formatChangelog = releases => { 8 | const changelog = ['# Changelog\n']; 9 | 10 | for (const release of releases) { 11 | const releaseTitle = release.name || release.tagName; 12 | 13 | changelog.push(`\n## [${releaseTitle}](${release.url}) (${release.createdAt.format('YYYY-MM-DD')})\n\n`); 14 | 15 | for (const { author, number, title, url } of release.pullRequests) { 16 | changelog.push(`- ${title} [\\#${number}](${url}) ([${author.login}](${author.url}))\n`); 17 | } 18 | } 19 | 20 | return changelog; 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import { Command } from 'commander'; 6 | import { formatChangelog } from './changelog-formatter.js'; 7 | import { lookItUpSync } from 'look-it-up'; 8 | import { readFileSync } from 'node:fs'; 9 | import ChangelogFetcher from './changelog-fetcher.js'; 10 | import ini from 'ini'; 11 | import path from 'node:path'; 12 | 13 | /** 14 | * Instances. 15 | */ 16 | 17 | const program = new Command(); 18 | 19 | /** 20 | * Command-line program definition. 21 | */ 22 | 23 | program 24 | .option('-b, --base-branch ', '[optional] specify the base branch name - master by default') 25 | .option('-f, --future-release ', '[optional] specify the next release version') 26 | .option( 27 | '-t, --future-release-tag ', 28 | '[optional] specify the next release tag name if it is different from the release version' 29 | ) 30 | .option( 31 | '-rtp, --release-tag-prefix ', 32 | '[optional] release tag prefix to consider when finding the latest release, useful for monorepos' 33 | ) 34 | .option( 35 | '-cfp, --changed-files-prefix ', 36 | '[optional] changed files prefix to consider when finding pull-requests, useful for monorepos' 37 | ) 38 | .option('-l, --labels ', '[optional] labels to filter pull requests by', val => val.split(',')) 39 | .option('-o, --owner ', '[optional] owner of the repository') 40 | .option('-r, --repo ', '[optional] name of the repository') 41 | .option('--rebuild', 'rebuild the full changelog', false) 42 | .description('Run GitHub changelog generator.') 43 | .parse(process.argv); 44 | 45 | /** 46 | * Options. 47 | */ 48 | 49 | const options = program.opts(); 50 | const gitDir = lookItUpSync('.git'); 51 | const { baseBranch = 'master', futureRelease, futureReleaseTag, labels, rebuild, releaseTagPrefix } = options; 52 | const token = process.env.GITHUB_TOKEN; 53 | let { changedFilesPrefix, owner, repo } = options; 54 | 55 | /** 56 | * Infer owner and repo from git config if not provided. 57 | */ 58 | 59 | if (!owner || !repo) { 60 | try { 61 | const gitconfig = readFileSync(path.join(gitDir, 'config'), 'utf-8'); 62 | const remoteOrigin = ini.parse(gitconfig)['remote "origin"']; 63 | const match = remoteOrigin.url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/); 64 | 65 | owner = owner || match[1]; 66 | repo = repo || match[2]; 67 | } catch (e) { 68 | process.stderr.write(` 69 | Failed to infer repository owner and name. 70 | Please use options --owner and --repo to manually set them. 71 | `); 72 | 73 | process.exit(1); 74 | } 75 | } 76 | 77 | /** 78 | * Infer changed files prefix from git directory. 79 | */ 80 | 81 | if (!changedFilesPrefix) { 82 | changedFilesPrefix = path.relative(path.resolve(gitDir, '..'), '.').split(path.sep).join(path.posix.sep); 83 | } 84 | 85 | /** 86 | * Run the changelog generator. 87 | */ 88 | 89 | async function run() { 90 | const fetcher = new ChangelogFetcher({ 91 | base: baseBranch, 92 | changedFilesPrefix, 93 | futureRelease, 94 | futureReleaseTag, 95 | labels, 96 | owner, 97 | releaseTagPrefix, 98 | repo, 99 | token 100 | }); 101 | 102 | const releases = await (rebuild ? fetcher.fetchFullChangelog() : fetcher.fetchLatestChangelog()); 103 | 104 | formatChangelog(releases).forEach(line => process.stdout.write(line)); 105 | } 106 | 107 | /** 108 | * Run. 109 | */ 110 | 111 | run(); 112 | -------------------------------------------------------------------------------- /test/changelog-fetcher.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import { jest } from '@jest/globals'; 6 | import ChangelogFetcher from '../src/changelog-fetcher.js'; 7 | import moment from 'moment'; 8 | import nock from 'nock'; 9 | 10 | /** 11 | * Test `ChangelogFetcher`. 12 | */ 13 | 14 | describe('ChangelogFetcher', () => { 15 | function getDataForRequest(requestBody, split = false) { 16 | const { query, variables } = requestBody; 17 | 18 | if (/query getLatestRelease\(/.test(query)) { 19 | return { 20 | data: { 21 | repository: { 22 | createdAt: moment('2017-10-22T12').toISOString(), 23 | latestRelease: { 24 | name: 'latest-release', 25 | tagCommit: { 26 | committedDate: moment('2018-10-22T12').toISOString() 27 | }, 28 | tagName: 'latest-release', 29 | url: 'latest-release-url' 30 | } 31 | } 32 | } 33 | }; 34 | } else if (/query getPullRequests\(/.test(query)) { 35 | const maybeIncludeFiles = files => (query.includes('files (') ? { files: { nodes: files } } : {}); 36 | const allNodes = [ 37 | { 38 | author: { login: 'quxfoo-user-login', url: 'quxfoo-user-url' }, 39 | mergedAt: moment('2018-10-24T10').toISOString(), 40 | number: 'quxfoo-number', 41 | title: 'quxfoo-title', 42 | updatedAt: moment('2018-10-24T10').toISOString(), 43 | url: 'quxfoo-url', 44 | ...maybeIncludeFiles([{ path: 'packages/quxfoo/index.js' }]) 45 | }, 46 | { 47 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 48 | mergedAt: moment('2018-10-23T10').toISOString(), 49 | number: 'foobar-number', 50 | title: 'foobar-title', 51 | updatedAt: moment('2018-10-23T10').toISOString(), 52 | url: 'foobar-url', 53 | ...maybeIncludeFiles([{ path: 'packages/foobar/index.js' }]) 54 | }, 55 | { 56 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 57 | mergedAt: moment('2018-10-22T20').toISOString(), 58 | number: 'foobiz-number', 59 | title: 'foobiz-title', 60 | updatedAt: moment('2018-10-22T20').toISOString(), 61 | url: 'foobiz-url', 62 | ...maybeIncludeFiles([{ path: 'packages/foobiz/index.js' }]) 63 | }, 64 | { 65 | author: { login: 'barbuz-user-login', url: 'barbuz-user-url' }, 66 | mergedAt: moment('2018-10-21T05').toISOString(), 67 | number: 'barbuz-number', 68 | title: 'barbuz-title', 69 | updatedAt: moment('2018-10-22T15').toISOString(), 70 | url: 'barbuz-url', 71 | ...maybeIncludeFiles([{ path: 'packages/barbuz/index.js' }]) 72 | }, 73 | { 74 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 75 | mergedAt: moment('2018-10-22T10').toISOString(), 76 | number: 'barbiz-number', 77 | title: 'barbiz-title', 78 | updatedAt: moment('2018-10-22T10').toISOString(), 79 | url: 'barbiz-url', 80 | ...maybeIncludeFiles([{ path: 'packages/barbiz/index.js' }]) 81 | } 82 | ]; 83 | 84 | // Return the full list of nodes if not testing cursor, else return the list in two parts. 85 | // We know we should pass the second part if the cursor variable is not empty. 86 | let nodes = allNodes; 87 | 88 | if (split) { 89 | nodes = variables.cursor ? allNodes.slice(2) : allNodes.slice(0, 2); 90 | } 91 | 92 | if (variables.labels.length) { 93 | nodes = [allNodes[0], allNodes[2], allNodes[4]]; 94 | } 95 | 96 | return { 97 | data: { 98 | repository: { 99 | pullRequests: { 100 | nodes, 101 | pageInfo: { 102 | endCursor: 'endCursorVal', 103 | hasNextPage: split && !variables.cursor 104 | } 105 | } 106 | } 107 | } 108 | }; 109 | } else if (/query getRepositoryCreatedAt\(/.test(query)) { 110 | return { 111 | data: { 112 | repository: { 113 | createdAt: moment('2018-10-20T12').toISOString() 114 | } 115 | } 116 | }; 117 | } else if (/query getReleases\(/.test(query)) { 118 | const allNodes = [ 119 | { 120 | name: 'foobar-name', 121 | tagCommit: { 122 | committedDate: moment('2018-10-23T12').toISOString() 123 | }, 124 | tagName: 'foobar-tagname', 125 | url: 'foobar-url' 126 | }, 127 | { 128 | name: 'bizbaz-name', 129 | tagCommit: { 130 | committedDate: moment('2018-10-22T12').toISOString() 131 | }, 132 | tagName: 'bizbaz-tagname', 133 | url: 'bizbaz-url' 134 | } 135 | ]; 136 | 137 | let nodes = allNodes; 138 | 139 | if (split) { 140 | nodes = variables.cursor ? allNodes.slice(1) : allNodes.slice(0, 1); 141 | } 142 | 143 | return { 144 | data: { 145 | repository: { 146 | releases: { 147 | nodes, 148 | pageInfo: { 149 | endCursor: 'endCursorVal', 150 | hasNextPage: split && !variables.cursor 151 | } 152 | } 153 | } 154 | } 155 | }; 156 | } 157 | 158 | throw new Error('Unexpected requestBody'); 159 | } 160 | 161 | describe('constructor()', () => { 162 | it('should set all defined fields', () => { 163 | const fetcher = new ChangelogFetcher({ 164 | base: 'foo', 165 | changedFilesPrefix: 'foz', 166 | futureRelease: 'bar', 167 | futureReleaseTag: 'baz', 168 | labels: 'bez', 169 | owner: 'biz', 170 | releaseTagPrefix: 'boz', 171 | repo: 'buz', 172 | token: 'qux' 173 | }); 174 | 175 | expect(fetcher.base).toEqual('foo'); 176 | expect(fetcher.changedFilesPrefix).toEqual('foz'); 177 | expect(fetcher.futureRelease).toEqual('bar'); 178 | expect(fetcher.futureReleaseTag).toEqual('baz'); 179 | expect(fetcher.labels).toEqual('bez'); 180 | expect(fetcher.owner).toEqual('biz'); 181 | expect(fetcher.releaseTagPrefix).toEqual('boz'); 182 | expect(fetcher.repo).toEqual('buz'); 183 | }); 184 | 185 | it('should set default values', () => { 186 | const fetcher = new ChangelogFetcher({ 187 | futureRelease: 'foo', 188 | token: 'bar' 189 | }); 190 | 191 | expect(fetcher.futureReleaseTag).toEqual('foo'); 192 | }); 193 | }); 194 | 195 | describe('fetchFullChangelog()', () => { 196 | it('should return a list with all releases and the pull requests associated to them', async () => { 197 | const fetcher = new ChangelogFetcher({ 198 | base: 'foo', 199 | owner: 'biz', 200 | repo: 'buz', 201 | token: 'qux' 202 | }); 203 | 204 | nock('https://api.github.com') 205 | .post('/graphql') 206 | .times(3) 207 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 208 | 209 | const releases = await fetcher.fetchFullChangelog(); 210 | 211 | expect(releases).toEqual([ 212 | { 213 | createdAt: moment(moment('2018-10-23T12').toISOString()), 214 | name: 'foobar-name', 215 | pullRequests: [ 216 | { 217 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 218 | mergedAt: moment('2018-10-23T10').toISOString(), 219 | number: 'foobar-number', 220 | title: 'foobar-title', 221 | updatedAt: moment('2018-10-23T10').toISOString(), 222 | url: 'foobar-url' 223 | }, 224 | { 225 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 226 | mergedAt: moment('2018-10-22T20').toISOString(), 227 | number: 'foobiz-number', 228 | title: 'foobiz-title', 229 | updatedAt: moment('2018-10-22T20').toISOString(), 230 | url: 'foobiz-url' 231 | } 232 | ], 233 | tagName: 'foobar-tagname', 234 | url: 'foobar-url' 235 | }, 236 | { 237 | createdAt: moment(moment('2018-10-22T12').toISOString()), 238 | name: 'bizbaz-name', 239 | pullRequests: [ 240 | { 241 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 242 | mergedAt: moment('2018-10-22T10').toISOString(), 243 | number: 'barbiz-number', 244 | title: 'barbiz-title', 245 | updatedAt: moment('2018-10-22T10').toISOString(), 246 | url: 'barbiz-url' 247 | }, 248 | { 249 | author: { login: 'barbuz-user-login', url: 'barbuz-user-url' }, 250 | mergedAt: moment('2018-10-21T05').toISOString(), 251 | number: 'barbuz-number', 252 | title: 'barbuz-title', 253 | updatedAt: moment('2018-10-22T15').toISOString(), 254 | url: 'barbuz-url' 255 | } 256 | ], 257 | tagName: 'bizbaz-tagname', 258 | url: 'bizbaz-url' 259 | } 260 | ]); 261 | }); 262 | 263 | it('should handle pagination correctly', async () => { 264 | const fetcher = new ChangelogFetcher({ 265 | base: 'foo', 266 | owner: 'biz', 267 | repo: 'buz', 268 | token: 'qux' 269 | }); 270 | 271 | nock('https://api.github.com') 272 | .post('/graphql') 273 | .times(5) 274 | .reply(200, (_, requestBody) => getDataForRequest(requestBody, true)); 275 | 276 | const releases = await fetcher.fetchFullChangelog(); 277 | 278 | expect(releases).toEqual([ 279 | { 280 | createdAt: moment(moment('2018-10-23T12').toISOString()), 281 | name: 'foobar-name', 282 | pullRequests: [ 283 | { 284 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 285 | mergedAt: moment('2018-10-23T10').toISOString(), 286 | number: 'foobar-number', 287 | title: 'foobar-title', 288 | updatedAt: moment('2018-10-23T10').toISOString(), 289 | url: 'foobar-url' 290 | }, 291 | { 292 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 293 | mergedAt: moment('2018-10-22T20').toISOString(), 294 | number: 'foobiz-number', 295 | title: 'foobiz-title', 296 | updatedAt: moment('2018-10-22T20').toISOString(), 297 | url: 'foobiz-url' 298 | } 299 | ], 300 | tagName: 'foobar-tagname', 301 | url: 'foobar-url' 302 | }, 303 | { 304 | createdAt: moment(moment('2018-10-22T12').toISOString()), 305 | name: 'bizbaz-name', 306 | pullRequests: [ 307 | { 308 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 309 | mergedAt: moment('2018-10-22T10').toISOString(), 310 | number: 'barbiz-number', 311 | title: 'barbiz-title', 312 | updatedAt: moment('2018-10-22T10').toISOString(), 313 | url: 'barbiz-url' 314 | }, 315 | { 316 | author: { login: 'barbuz-user-login', url: 'barbuz-user-url' }, 317 | mergedAt: moment('2018-10-21T05').toISOString(), 318 | number: 'barbuz-number', 319 | title: 'barbuz-title', 320 | updatedAt: moment('2018-10-22T15').toISOString(), 321 | url: 'barbuz-url' 322 | } 323 | ], 324 | tagName: 'bizbaz-tagname', 325 | url: 'bizbaz-url' 326 | } 327 | ]); 328 | }); 329 | 330 | it('should filter releases by the given releaseTagPrefix', async () => { 331 | const fetcher = new ChangelogFetcher({ 332 | base: 'foo', 333 | owner: 'biz', 334 | releaseTagPrefix: 'foobar-', 335 | repo: 'buz', 336 | token: 'qux' 337 | }); 338 | 339 | nock('https://api.github.com') 340 | .post('/graphql') 341 | .times(3) 342 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 343 | 344 | const releases = await fetcher.fetchFullChangelog(); 345 | 346 | expect(releases).toEqual([ 347 | { 348 | createdAt: moment(moment('2018-10-23T12').toISOString()), 349 | name: 'foobar-name', 350 | pullRequests: [ 351 | { 352 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 353 | mergedAt: moment('2018-10-23T10').toISOString(), 354 | number: 'foobar-number', 355 | title: 'foobar-title', 356 | updatedAt: moment('2018-10-23T10').toISOString(), 357 | url: 'foobar-url' 358 | }, 359 | { 360 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 361 | mergedAt: moment('2018-10-22T20').toISOString(), 362 | number: 'foobiz-number', 363 | title: 'foobiz-title', 364 | updatedAt: moment('2018-10-22T20').toISOString(), 365 | url: 'foobiz-url' 366 | }, 367 | { 368 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 369 | mergedAt: moment('2018-10-22T10').toISOString(), 370 | number: 'barbiz-number', 371 | title: 'barbiz-title', 372 | updatedAt: moment('2018-10-22T10').toISOString(), 373 | url: 'barbiz-url' 374 | }, 375 | { 376 | author: { login: 'barbuz-user-login', url: 'barbuz-user-url' }, 377 | mergedAt: moment('2018-10-21T05').toISOString(), 378 | number: 'barbuz-number', 379 | title: 'barbuz-title', 380 | updatedAt: moment('2018-10-22T15').toISOString(), 381 | url: 'barbuz-url' 382 | } 383 | ], 384 | tagName: 'foobar-tagname', 385 | url: 'foobar-url' 386 | } 387 | ]); 388 | }); 389 | 390 | it('should filter pull requests by the given labels', async () => { 391 | const fetcher = new ChangelogFetcher({ 392 | base: 'foo', 393 | futureRelease: 'futRel', 394 | labels: ['fizz', 'fuzz'], 395 | owner: 'biz', 396 | repo: 'buz', 397 | token: 'qux' 398 | }); 399 | 400 | nock('https://api.github.com') 401 | .post('/graphql') 402 | .times(3) 403 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 404 | 405 | const releases = await fetcher.fetchFullChangelog(); 406 | 407 | expect(releases).toEqual([ 408 | { 409 | createdAt: moment(moment('2018-10-23T12').toISOString()), 410 | name: 'foobar-name', 411 | pullRequests: [ 412 | { 413 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 414 | mergedAt: moment('2018-10-22T20').toISOString(), 415 | number: 'foobiz-number', 416 | title: 'foobiz-title', 417 | updatedAt: moment('2018-10-22T20').toISOString(), 418 | url: 'foobiz-url' 419 | } 420 | ], 421 | tagName: 'foobar-tagname', 422 | url: 'foobar-url' 423 | }, 424 | { 425 | createdAt: moment(moment('2018-10-22T12').toISOString()), 426 | name: 'bizbaz-name', 427 | pullRequests: [ 428 | { 429 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 430 | mergedAt: moment('2018-10-22T10').toISOString(), 431 | number: 'barbiz-number', 432 | title: 'barbiz-title', 433 | updatedAt: moment('2018-10-22T10').toISOString(), 434 | url: 'barbiz-url' 435 | } 436 | ], 437 | tagName: 'bizbaz-tagname', 438 | url: 'bizbaz-url' 439 | } 440 | ]); 441 | }); 442 | 443 | it('should filter pull requests by the given changedFilesPrefix', async () => { 444 | const fetcher = new ChangelogFetcher({ 445 | base: 'foo', 446 | changedFilesPrefix: 'packages/foobar/', 447 | futureRelease: 'futRel', 448 | owner: 'biz', 449 | repo: 'buz', 450 | token: 'qux' 451 | }); 452 | 453 | nock('https://api.github.com') 454 | .post('/graphql') 455 | .times(3) 456 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 457 | 458 | const releases = await fetcher.fetchFullChangelog(); 459 | 460 | expect(releases).toEqual([ 461 | { 462 | createdAt: moment(moment('2018-10-23T12').toISOString()), 463 | name: 'foobar-name', 464 | pullRequests: [ 465 | { 466 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 467 | mergedAt: moment('2018-10-23T10').toISOString(), 468 | number: 'foobar-number', 469 | title: 'foobar-title', 470 | updatedAt: moment('2018-10-23T10').toISOString(), 471 | url: 'foobar-url' 472 | } 473 | ], 474 | tagName: 'foobar-tagname', 475 | url: 'foobar-url' 476 | }, 477 | { 478 | createdAt: moment(moment('2018-10-22T12').toISOString()), 479 | name: 'bizbaz-name', 480 | pullRequests: [], 481 | tagName: 'bizbaz-tagname', 482 | url: 'bizbaz-url' 483 | } 484 | ]); 485 | }); 486 | }); 487 | 488 | describe('fetchLatestChangelog()', () => { 489 | it('should not return any releases unless you specify a futureRelease', async () => { 490 | const fetcher = new ChangelogFetcher({ 491 | base: 'foo', 492 | owner: 'biz', 493 | repo: 'buz', 494 | token: 'qux' 495 | }); 496 | const releases = await fetcher.fetchLatestChangelog(); 497 | 498 | expect(releases).toEqual([]); 499 | }); 500 | 501 | it('should throw an error if the futureReleaseTag is already the latest release', async () => { 502 | const fetcher = new ChangelogFetcher({ 503 | base: 'foo', 504 | futureRelease: 'bar', 505 | owner: 'biz', 506 | repo: 'buz', 507 | token: 'qux' 508 | }); 509 | 510 | jest.spyOn(fetcher, 'client').mockReturnValue({ 511 | repository: { 512 | latestRelease: { 513 | tagCommit: { 514 | committedDate: moment('2018-10-22T12').toISOString() 515 | }, 516 | tagName: 'bar' 517 | } 518 | } 519 | }); 520 | 521 | try { 522 | await fetcher.fetchLatestChangelog(); 523 | 524 | jest.fail(); 525 | } catch (e) { 526 | expect(e).toBeInstanceOf(Error); 527 | expect(e.message).toBe('Changelog already on the latest release'); 528 | } 529 | }); 530 | 531 | it('should return a list with the last release and the pull requests done after the last release was created', async () => { 532 | const fetcher = new ChangelogFetcher({ 533 | base: 'foo', 534 | futureRelease: 'futRel', 535 | owner: 'biz', 536 | repo: 'buz', 537 | token: 'qux' 538 | }); 539 | 540 | nock('https://api.github.com') 541 | .post('/graphql') 542 | .times(2) 543 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 544 | 545 | const releases = await fetcher.fetchLatestChangelog(); 546 | 547 | expect(releases).toEqual([ 548 | { 549 | createdAt: expect.any(moment), 550 | name: 'futRel', 551 | pullRequests: [ 552 | { 553 | author: { login: 'quxfoo-user-login', url: 'quxfoo-user-url' }, 554 | mergedAt: moment('2018-10-24T10').toISOString(), 555 | number: 'quxfoo-number', 556 | title: 'quxfoo-title', 557 | updatedAt: moment('2018-10-24T10').toISOString(), 558 | url: 'quxfoo-url' 559 | }, 560 | { 561 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 562 | mergedAt: moment('2018-10-23T10').toISOString(), 563 | number: 'foobar-number', 564 | title: 'foobar-title', 565 | updatedAt: moment('2018-10-23T10').toISOString(), 566 | url: 'foobar-url' 567 | }, 568 | { 569 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 570 | mergedAt: moment('2018-10-22T20').toISOString(), 571 | number: 'foobiz-number', 572 | title: 'foobiz-title', 573 | updatedAt: moment('2018-10-22T20').toISOString(), 574 | url: 'foobiz-url' 575 | } 576 | ], 577 | tagName: 'futRel', 578 | url: 'https://github.com/biz/buz/releases/tag/futRel' 579 | } 580 | ]); 581 | }); 582 | 583 | it('should handle pagination correctly', async () => { 584 | const fetcher = new ChangelogFetcher({ 585 | base: 'foo', 586 | futureRelease: 'futRel', 587 | owner: 'biz', 588 | repo: 'buz', 589 | token: 'qux' 590 | }); 591 | 592 | nock('https://api.github.com') 593 | .post('/graphql') 594 | .times(3) 595 | .reply(200, (_, requestBody) => getDataForRequest(requestBody, true)); 596 | 597 | const releases = await fetcher.fetchLatestChangelog(); 598 | 599 | expect(releases).toEqual([ 600 | { 601 | createdAt: expect.any(moment), 602 | name: 'futRel', 603 | pullRequests: [ 604 | { 605 | author: { login: 'quxfoo-user-login', url: 'quxfoo-user-url' }, 606 | mergedAt: moment('2018-10-24T10').toISOString(), 607 | number: 'quxfoo-number', 608 | title: 'quxfoo-title', 609 | updatedAt: moment('2018-10-24T10').toISOString(), 610 | url: 'quxfoo-url' 611 | }, 612 | { 613 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 614 | mergedAt: moment('2018-10-23T10').toISOString(), 615 | number: 'foobar-number', 616 | title: 'foobar-title', 617 | updatedAt: moment('2018-10-23T10').toISOString(), 618 | url: 'foobar-url' 619 | }, 620 | { 621 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 622 | mergedAt: moment('2018-10-22T20').toISOString(), 623 | number: 'foobiz-number', 624 | title: 'foobiz-title', 625 | updatedAt: moment('2018-10-22T20').toISOString(), 626 | url: 'foobiz-url' 627 | } 628 | ], 629 | tagName: 'futRel', 630 | url: 'https://github.com/biz/buz/releases/tag/futRel' 631 | } 632 | ]); 633 | }); 634 | 635 | it('should filter releases by the given releaseTagPrefix', async () => { 636 | const fetcher = new ChangelogFetcher({ 637 | base: 'foo', 638 | futureRelease: 'futRel', 639 | owner: 'biz', 640 | releaseTagPrefix: 'foobar-', 641 | repo: 'buz', 642 | token: 'qux' 643 | }); 644 | 645 | nock('https://api.github.com') 646 | .post('/graphql') 647 | .times(2) 648 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 649 | 650 | const releases = await fetcher.fetchLatestChangelog(); 651 | 652 | expect(releases).toEqual([ 653 | { 654 | createdAt: expect.any(moment), 655 | name: 'futRel', 656 | pullRequests: [ 657 | { 658 | author: { login: 'quxfoo-user-login', url: 'quxfoo-user-url' }, 659 | mergedAt: moment('2018-10-24T10').toISOString(), 660 | number: 'quxfoo-number', 661 | title: 'quxfoo-title', 662 | updatedAt: moment('2018-10-24T10').toISOString(), 663 | url: 'quxfoo-url' 664 | } 665 | ], 666 | tagName: 'futRel', 667 | url: 'https://github.com/biz/buz/releases/tag/futRel' 668 | } 669 | ]); 670 | }); 671 | 672 | it('should filter pull requests by the given labels', async () => { 673 | const fetcher = new ChangelogFetcher({ 674 | base: 'foo', 675 | futureRelease: 'futRel', 676 | labels: ['fizz', 'fuzz'], 677 | owner: 'biz', 678 | repo: 'buz', 679 | token: 'qux' 680 | }); 681 | 682 | nock('https://api.github.com') 683 | .post('/graphql') 684 | .times(2) 685 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 686 | 687 | const releases = await fetcher.fetchLatestChangelog(); 688 | 689 | expect(releases).toEqual([ 690 | { 691 | createdAt: expect.any(moment), 692 | name: 'futRel', 693 | pullRequests: [ 694 | { 695 | author: { login: 'quxfoo-user-login', url: 'quxfoo-user-url' }, 696 | mergedAt: moment('2018-10-24T10').toISOString(), 697 | number: 'quxfoo-number', 698 | title: 'quxfoo-title', 699 | updatedAt: moment('2018-10-24T10').toISOString(), 700 | url: 'quxfoo-url' 701 | }, 702 | { 703 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 704 | mergedAt: moment('2018-10-22T20').toISOString(), 705 | number: 'foobiz-number', 706 | title: 'foobiz-title', 707 | updatedAt: moment('2018-10-22T20').toISOString(), 708 | url: 'foobiz-url' 709 | } 710 | ], 711 | tagName: 'futRel', 712 | url: 'https://github.com/biz/buz/releases/tag/futRel' 713 | } 714 | ]); 715 | }); 716 | 717 | it('should filter pull requests by the given changedFilesPrefix', async () => { 718 | const fetcher = new ChangelogFetcher({ 719 | base: 'foo', 720 | changedFilesPrefix: 'packages/foobar/', 721 | futureRelease: 'futRel', 722 | owner: 'biz', 723 | repo: 'buz', 724 | token: 'qux' 725 | }); 726 | 727 | nock('https://api.github.com') 728 | .post('/graphql') 729 | .times(2) 730 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 731 | 732 | const releases = await fetcher.fetchLatestChangelog(); 733 | 734 | expect(releases).toEqual([ 735 | { 736 | createdAt: expect.any(moment), 737 | name: 'futRel', 738 | pullRequests: [ 739 | { 740 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 741 | mergedAt: moment('2018-10-23T10').toISOString(), 742 | number: 'foobar-number', 743 | title: 'foobar-title', 744 | updatedAt: moment('2018-10-23T10').toISOString(), 745 | url: 'foobar-url' 746 | } 747 | ], 748 | tagName: 'futRel', 749 | url: 'https://github.com/biz/buz/releases/tag/futRel' 750 | } 751 | ]); 752 | }); 753 | 754 | it('should stop iterating when a pull request has an updated at that is before the start timestamp', async () => { 755 | const fetcher = new ChangelogFetcher({ 756 | base: 'foo', 757 | futureRelease: 'futRel', 758 | owner: 'biz', 759 | repo: 'buz', 760 | token: 'qux' 761 | }); 762 | const startDate = moment('2018-10-23T08'); 763 | 764 | nock('https://api.github.com') 765 | .post('/graphql') 766 | .times(1) 767 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 768 | 769 | jest.spyOn(fetcher, 'getLatestRelease').mockReturnValue({ tagCommit: { committedDate: startDate } }); 770 | jest.spyOn(startDate, 'isAfter'); 771 | jest.spyOn(startDate, 'isBefore'); 772 | 773 | const releases = await fetcher.fetchLatestChangelog(); 774 | 775 | expect(releases[0].pullRequests).toHaveLength(2); 776 | expect(startDate.isAfter).toHaveBeenCalledTimes(1); 777 | expect(startDate.isAfter).toHaveReturnedWith(true); 778 | expect(startDate.isBefore).toHaveBeenCalledTimes(3); 779 | expect(startDate.isBefore).toHaveNthReturnedWith(1, true); 780 | expect(startDate.isBefore).toHaveNthReturnedWith(2, true); 781 | expect(startDate.isBefore).toHaveNthReturnedWith(3, false); 782 | }); 783 | 784 | it('should stop iterating when no more pull requests exist', async () => { 785 | const fetcher = new ChangelogFetcher({ 786 | base: 'foo', 787 | futureRelease: 'futRel', 788 | owner: 'biz', 789 | repo: 'buz', 790 | token: 'qux' 791 | }); 792 | const startDate = moment('2018-10-10T08'); 793 | 794 | nock('https://api.github.com') 795 | .post('/graphql') 796 | .times(1) 797 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 798 | 799 | jest.spyOn(fetcher, 'getLatestRelease').mockReturnValue({ tagCommit: { committedDate: startDate } }); 800 | jest.spyOn(startDate, 'isAfter'); 801 | jest.spyOn(startDate, 'isBefore'); 802 | 803 | const releases = await fetcher.fetchLatestChangelog(); 804 | 805 | expect(releases[0].pullRequests).toHaveLength(5); 806 | expect(startDate.isAfter).not.toHaveBeenCalled(); 807 | expect(startDate.isBefore).toHaveBeenCalledTimes(5); 808 | expect(startDate.isBefore).toHaveNthReturnedWith(1, true); 809 | expect(startDate.isBefore).toHaveNthReturnedWith(2, true); 810 | expect(startDate.isBefore).toHaveNthReturnedWith(3, true); 811 | expect(startDate.isBefore).toHaveNthReturnedWith(4, true); 812 | expect(startDate.isBefore).toHaveNthReturnedWith(5, true); 813 | }); 814 | }); 815 | 816 | describe('getLatestRelease()', () => { 817 | it('should return the latest release', async () => { 818 | const fetcher = new ChangelogFetcher({ 819 | owner: 'biz', 820 | repo: 'buz', 821 | token: 'qux' 822 | }); 823 | 824 | nock('https://api.github.com') 825 | .post('/graphql') 826 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 827 | 828 | const result = await fetcher.getLatestRelease(); 829 | 830 | expect(result).toEqual({ 831 | name: 'latest-release', 832 | tagCommit: { 833 | committedDate: moment.utc(moment('2018-10-22T12').toISOString()) 834 | }, 835 | tagName: 'latest-release', 836 | url: 'latest-release-url' 837 | }); 838 | }); 839 | 840 | it('should return a mocked latest release if the repository has no releases', async () => { 841 | const fetcher = new ChangelogFetcher({ 842 | owner: 'biz', 843 | repo: 'buz', 844 | token: 'qux' 845 | }); 846 | 847 | nock('https://api.github.com') 848 | .post('/graphql') 849 | .reply(200, (_, requestBody) => { 850 | const mockData = getDataForRequest(requestBody); 851 | 852 | mockData.data.repository.latestRelease = undefined; 853 | 854 | return mockData; 855 | }); 856 | 857 | const result = await fetcher.getLatestRelease(); 858 | 859 | expect(result).toEqual({ 860 | tagCommit: { 861 | committedDate: moment.utc(moment('2017-10-22T12').toISOString()) 862 | } 863 | }); 864 | }); 865 | }); 866 | 867 | describe('getLatestReleaseByTagPrefix()', () => { 868 | it('should return the latest release that starts with the given prefix', async () => { 869 | const fetcher = new ChangelogFetcher({ 870 | owner: 'biz', 871 | releaseTagPrefix: 'foobar-', 872 | repo: 'buz', 873 | token: 'qux' 874 | }); 875 | 876 | nock('https://api.github.com') 877 | .post('/graphql') 878 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 879 | 880 | const result = await fetcher.getLatestReleaseByTagPrefix(); 881 | 882 | expect(result).toEqual({ 883 | name: 'foobar-name', 884 | tagCommit: { 885 | committedDate: moment.utc(moment('2018-10-23T12').toISOString()) 886 | }, 887 | tagName: 'foobar-tagname', 888 | url: 'foobar-url' 889 | }); 890 | }); 891 | 892 | it('should return a mocked latest release if the repository has no releases', async () => { 893 | const fetcher = new ChangelogFetcher({ 894 | owner: 'biz', 895 | releaseTagPrefix: 'foobar-', 896 | repo: 'buz', 897 | token: 'qux' 898 | }); 899 | 900 | nock('https://api.github.com') 901 | .post('/graphql') 902 | .reply(200, (_, requestBody) => { 903 | const mockData = getDataForRequest(requestBody); 904 | 905 | mockData.data.repository.releases.nodes = []; 906 | 907 | return mockData; 908 | }); 909 | 910 | nock('https://api.github.com') 911 | .post('/graphql') 912 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 913 | 914 | const result = await fetcher.getLatestReleaseByTagPrefix(); 915 | 916 | expect(result).toEqual({ 917 | tagCommit: { 918 | committedDate: moment.utc(moment('2018-10-20T12').toISOString()) 919 | } 920 | }); 921 | }); 922 | }); 923 | 924 | describe('getRepositoryCreatedAt()', () => { 925 | it('should return the date the repository was created', async () => { 926 | const fetcher = new ChangelogFetcher({ 927 | owner: 'biz', 928 | repo: 'buz', 929 | token: 'qux' 930 | }); 931 | 932 | nock('https://api.github.com') 933 | .post('/graphql') 934 | .reply(200, (_, requestBody) => getDataForRequest(requestBody)); 935 | 936 | const result = await fetcher.getRepositoryCreatedAt(); 937 | 938 | expect(result).toEqual(moment.utc(moment('2018-10-20T12').toISOString())); 939 | }); 940 | }); 941 | }); 942 | -------------------------------------------------------------------------------- /test/changelog-formatter.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import { formatChangelog } from '../src/changelog-formatter.js'; 6 | import moment from 'moment'; 7 | 8 | /** 9 | * Test `ChangelogFormatter`. 10 | */ 11 | 12 | describe('ChangelogFormatter', () => { 13 | describe('formatChangelog()', () => { 14 | it('should return an array of lines for releases and pull requests in the expected format', () => { 15 | const releases = [ 16 | { 17 | createdAt: moment('2018-10-23'), 18 | name: 'foo-name', 19 | pullRequests: [ 20 | { 21 | author: { login: 'foobar-user-login', url: 'foobar-user-url' }, 22 | number: 'foobar-number', 23 | title: 'foobar-title', 24 | url: 'foobar-url' 25 | }, 26 | { 27 | author: { login: 'foobiz-user-login', url: 'foobiz-user-url' }, 28 | number: 'foobiz-number', 29 | title: 'foobiz-title', 30 | url: 'foobiz-url' 31 | } 32 | ], 33 | tagName: 'foo-tag', 34 | url: 'foo-url' 35 | }, 36 | { 37 | createdAt: moment('2018-10-22'), 38 | pullRequests: [ 39 | { 40 | author: { login: 'barbiz-user-login', url: 'barbiz-user-url' }, 41 | number: 'barbiz-number', 42 | title: 'barbiz-title', 43 | url: 'barbiz-url' 44 | }, 45 | { 46 | author: { login: 'barbuz-user-login', url: 'barbuz-user-url' }, 47 | number: 'barbuz-number', 48 | title: 'barbuz-title', 49 | url: 'barbuz-url' 50 | } 51 | ], 52 | tagName: 'bar-tag', 53 | url: 'bar-url' 54 | }, 55 | { 56 | createdAt: moment('2018-10-21'), 57 | pullRequests: [ 58 | { 59 | author: { login: 'foobuz-user-login', url: 'foobuz-user-url' }, 60 | number: 'foobuz-number', 61 | title: 'foobuz-title', 62 | url: 'foobuz-url' 63 | }, 64 | { 65 | author: { login: 'foobaz-user-login', url: 'foobaz-user-url' }, 66 | number: 'foobaz-number', 67 | title: 'foobaz-title', 68 | url: 'foobaz-url' 69 | } 70 | ], 71 | tagName: 'biz-tag', 72 | url: 'bar-url' 73 | }, 74 | { 75 | createdAt: moment('2018-10-20'), 76 | name: 'qux-name', 77 | pullRequests: [ 78 | { 79 | author: { login: 'fooqux-user-login', url: 'fooqux-user-url' }, 80 | number: 'fooqux-number', 81 | title: 'fooqux-title', 82 | url: 'fooqux-url' 83 | } 84 | ], 85 | url: 'bar-url' 86 | } 87 | ]; 88 | 89 | expect(formatChangelog(releases)).toEqual([ 90 | '# Changelog\n', 91 | '\n## [foo-name](foo-url) (2018-10-23)\n\n', 92 | '- foobar-title [\\#foobar-number](foobar-url) ([foobar-user-login](foobar-user-url))\n', 93 | '- foobiz-title [\\#foobiz-number](foobiz-url) ([foobiz-user-login](foobiz-user-url))\n', 94 | '\n## [bar-tag](bar-url) (2018-10-22)\n\n', 95 | '- barbiz-title [\\#barbiz-number](barbiz-url) ([barbiz-user-login](barbiz-user-url))\n', 96 | '- barbuz-title [\\#barbuz-number](barbuz-url) ([barbuz-user-login](barbuz-user-url))\n', 97 | '\n## [biz-tag](bar-url) (2018-10-21)\n\n', 98 | '- foobuz-title [\\#foobuz-number](foobuz-url) ([foobuz-user-login](foobuz-user-url))\n', 99 | '- foobaz-title [\\#foobaz-number](foobaz-url) ([foobaz-user-login](foobaz-user-url))\n', 100 | '\n## [qux-name](bar-url) (2018-10-20)\n\n', 101 | '- fooqux-title [\\#fooqux-number](fooqux-url) ([fooqux-user-login](fooqux-user-url))\n' 102 | ]); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import nock from 'nock'; 6 | 7 | /** 8 | * Disable any type of net connection. 9 | */ 10 | 11 | nock.disableNetConnect(); 12 | 13 | /** 14 | * Check all external mocks have been called. 15 | */ 16 | 17 | afterEach(() => { 18 | const pendingMocks = nock.pendingMocks(); 19 | 20 | if (pendingMocks.length) { 21 | nock.cleanAll(); 22 | 23 | throw new Error(`Unexpected pending mocks ${JSON.stringify(pendingMocks)}`); 24 | } 25 | }); 26 | --------------------------------------------------------------------------------