├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── pull_request.yml
│ └── release.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── changelog-enforcer.test.js
├── client.test.js
├── context-extractor.test.js
├── env.js
├── label-extractor.test.js
├── test_pull_request.json
└── version-extractor.test.js
├── action.yml
├── bin
└── cut-release.sh
├── coverage
└── badge.svg
├── dist
└── index.js
├── eslint.config.mjs
├── example-workflows
├── with-different-changelog-path.yaml
├── with-different-token.yaml
├── with-expected-latest-version-custom-pattern.yaml
└── with-expected-latest-version.yaml
├── index.js
├── jest.config.js
├── package-lock.json
├── package.json
└── src
├── changelog-enforcer.js
├── client.js
├── context-extractor.js
├── label-extractor.js
└── version-extractor.js
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for your interest in contributing! This is a very small and specific project - we would like to keep it that way.
4 |
5 | If you have an idea for a new feature, please open an [issue](https://github.com/dangoslen/changelog-enforcer/issues/new) first and discuss your idea for enhancement.
6 |
7 | If you have run into a problem, likewise open an [issue](https://github.com/dangoslen/changelog-enforcer/issues/new) and we will address it as best as we see fit.
8 |
9 | ## Development
10 |
11 | Currently, this project uses vanilla javascript via `node`. Dependencies are managed via `npm`.
12 |
13 | ### Installing
14 |
15 | After cloning this repository, run
16 |
17 | ```
18 | npm install
19 | ```
20 |
21 | ### Building
22 | ```
23 | npm run package
24 | ```
25 |
26 | ### Tests
27 | ```
28 | npm test
29 | ```
30 |
31 | This will run `npm lint` and lint code with [ESLint](https://eslint.org/)
32 |
33 | ### Changelog
34 |
35 | Any notable changes to functionality or updates to dependencies should be added into the [CHANGELOG](../CHANGELOG.md). For an overview of what to write, take a look at the [KeepAChangelog](https://keepachangelog.com/en/1.0.0/) guide.
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **A link to or sample of your workflow**
24 | Provide the workflow you are having issues with
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: dangoslen
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
2 | version: 2
3 | updates:
4 | - package-ecosystem: "npm"
5 | directory: "/"
6 | labels:
7 | - dependabot
8 | - javascript
9 | - verified
10 | open-pull-requests-limit: 2
11 | schedule:
12 | interval: "weekly"
13 | target-branch: "main"
14 |
15 | - package-ecosystem: "github-actions"
16 | directory: "/"
17 | labels:
18 | - dependabot
19 | - github-actions
20 | - verified
21 | open-pull-requests-limit: 2
22 | schedule:
23 | interval: "weekly"
24 | target-branch: "main"
--------------------------------------------------------------------------------
/.github/workflows/pull_request.yml:
--------------------------------------------------------------------------------
1 | name: "Pull Request Workflow"
2 | on:
3 | pull_request_target:
4 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
5 |
6 | jobs:
7 |
8 | # validates that the pull request is trusted
9 | verify:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - run: |
13 | VERIFIED_LABEL=${{ contains(github.event.pull_request.labels.*.name, 'verified') }}
14 | if [[ ( $VERIFIED_LABEL == 'false' ) ]]; then
15 | echo "Pull request is not from a trusted source!"
16 | exit 1
17 | fi
18 |
19 | # unit tests
20 | unit-tests:
21 | needs: [ verify ]
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4.1.2
25 | with:
26 | ref: ${{ github.event.pull_request.head.ref }}
27 | repository: ${{ github.event.pull_request.head.repo.full_name }}
28 |
29 | - run: npm install
30 | - run: npm run all
31 |
32 | # test action works running from the graph
33 | enforce-changelog:
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v4.1.2
37 | with:
38 | ref: ${{ github.event.pull_request.head.ref }}
39 | repository: ${{ github.event.pull_request.head.repo.full_name }}
40 | token: ${{ secrets.ACTION_TOKEN }}
41 |
42 | - id: read_version
43 | run: |
44 | echo "version=$(jq -r ".version" package.json)" >> $GITHUB_OUTPUT
45 | echo "tag=v$(jq -r ".version" package.json)" >> $GITHUB_OUTPUT
46 |
47 | - uses: dangoslen/dependabot-changelog-helper@v3
48 | with:
49 | activationLabel: 'dependabot'
50 |
51 | - uses: stefanzweifel/git-auto-commit-action@v5.0.1
52 | with:
53 | commit_message: "Update changelog"
54 |
55 | - id: changelog-enforcer
56 | uses: ./
57 | with:
58 | skipLabels: "skip-changelog"
59 | expectedLatestVersion: ${{ steps.read_version.outputs.tag }}
60 |
61 | - if: failure()
62 | uses: thollander/actions-comment-pull-request@v2
63 | with:
64 | message: |
65 | Hey @${{ github.event.pull_request.user.login }}, the Changelog Enforcer failed. Can you take a look at the error below and correct it? Thanks!
66 |
67 | ```
68 | ${{ steps.changelog-enforcer.outputs.errorMessage }}
69 | ```
70 | comment_tag: "changelog-failed"
71 |
72 | - id: changelog_reader
73 | uses: mindsers/changelog-reader-action@v2
74 | with:
75 | version: "${{ steps.read_version.outputs.tag }}"
76 | path: ./CHANGELOG.md
77 |
78 | - id: check_release
79 | run: |
80 | TAG=$(git ls-remote --tags origin | grep ${{ steps.read_version.outputs.tag }} | cat )
81 | MISSING=$([[ -z "$TAG" ]] && echo 'true' || echo 'false')
82 | echo "missing=$MISSING" >> $GITHUB_OUTPUT
83 |
84 | - if: ${{ steps.check_release.outputs.missing == 'true' }}
85 | uses: thollander/actions-comment-pull-request@v2
86 | with:
87 | message: |
88 |
89 | Preview of Release Notes to be Created
90 |
91 | ${{ steps.changelog_reader.outputs.changes }}
92 |
93 |
94 | comment_tag: "relase-note-preview"
95 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: "Release Workflow"
2 | on:
3 | push:
4 | branches:
5 | - 'releases/v1.6'
6 | - 'releases/v2.3'
7 | - 'releases/v3.3.0'
8 | - 'main'
9 |
10 | jobs:
11 | release:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4.1.2
15 |
16 | - name: read versions
17 | id: read-version
18 | run: |
19 | echo "version=$(jq -r ".version" package.json)" >> $GITHUB_OUTPUT
20 | echo "tag=v$(jq -r ".version" package.json)" >> $GITHUB_OUTPUT
21 | echo "major_tag=v$(jq -r ".version" package.json | cut -d '.' -f 1)" >> $GITHUB_OUTPUT
22 |
23 | - name: read changelog entry for version
24 | id: changelog_reader
25 | uses: mindsers/changelog-reader-action@v2
26 | with:
27 | version: "${{ steps.read-version.outputs.tag }}"
28 | path: ./CHANGELOG.md
29 |
30 | - name: check for existing release
31 | id: check_release
32 | run: |
33 | TAG=$(git ls-remote --tags origin | grep ${{ steps.read-version.outputs.tag }} | cat)
34 | MISSING=$([[ -z "$TAG" ]] && echo 'true' || echo 'false')
35 | echo "missing=$MISSING" >> $GITHUB_OUTPUT
36 |
37 | - name: create release
38 | if: ${{ steps.check_release.outputs.missing == 'true' }}
39 | id: create_release
40 | uses: ncipollo/release-action@v1.14.0
41 | with:
42 | tag: "${{ steps.read-version.outputs.tag }}"
43 | name: Changelog Enforcer ${{ steps.read-version.outputs.version }}
44 | body: ${{ steps.changelog_reader.outputs.changes }}
45 | draft: false
46 | prerelease: false
47 |
48 | - name: update major version tag
49 | if: ${{ steps.check_release.outputs.missing == 'true' }}
50 | uses: richardsimko/update-tag@v1
51 | with:
52 | tag_name: "${{ steps.read-version.outputs.major_tag }}"
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | # Editors
4 | .vscode
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Other Dependency directories
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 |
64 | # next.js build output
65 | .next
66 |
67 | # Mac
68 | .DS_Store
69 |
70 | !coverage/badge.svg
71 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.11.0
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
4 |
5 | ## [UNRELEASED]
6 |
7 | ### Dependencies
8 | - Bump `stefanzweifel/git-auto-commit-action` from 5.0.0 to 5.0.1 ([#287](https://github.com/dangoslen/changelog-enforcer/pull/287))
9 | - Bump `eslint` from 8.57.0 to 9.7.0 ([#288](https://github.com/dangoslen/changelog-enforcer/pull/288))
10 |
11 | ## [v3.6.1]
12 | ### Changed
13 | - Fix Github Actions Annotations ([#281](https://github.com/dangoslen/changelog-enforcer/pull/281))
14 |
15 | ### Fixed
16 | - Handle `skipLabels` that contains emojis by properly looking for `:` characters in the label extractor regex (fixes #284)
17 |
18 | ### Dependencies
19 | - Bump `eslint` from 8.56.0 to 8.57.0 ([#282](https://github.com/dangoslen/changelog-enforcer/pull/282))
20 | - Bump `actions/checkout` from 4.1.1 to 4.1.2 ([#283](https://github.com/dangoslen/changelog-enforcer/pull/283))
21 |
22 | ## [v3.6.0]
23 | ### Changed
24 | - Now runs on Node 20
25 | - Updates `.nvmrc` to set the version
26 | - Updates node version in `action.yml`
27 | ### Dependencies
28 | - Bump `node-fetch` from 2.6.12 to 2.7.0 ([#264](https://github.com/dangoslen/changelog-enforcer/pull/264), [#270](https://github.com/dangoslen/changelog-enforcer/pull/270))
29 | - Bump `actions/checkout` from 3.5.3 to 4.1.1 ([#266](https://github.com/dangoslen/changelog-enforcer/pull/266), [#267](https://github.com/dangoslen/changelog-enforcer/pull/267), [#271](https://github.com/dangoslen/changelog-enforcer/pull/271), [#275](https://github.com/dangoslen/changelog-enforcer/pull/275))
30 | - Bump `@vercel/ncc` from 0.36.1 to 0.38.1 ([#268](https://github.com/dangoslen/changelog-enforcer/pull/268), [#276](https://github.com/dangoslen/changelog-enforcer/pull/276))
31 | - Bump `jest` from 29.6.2 to 29.7.0 ([#269](https://github.com/dangoslen/changelog-enforcer/pull/269))
32 | - Bump `stefanzweifel/git-auto-commit-action` from 4.16.0 to 5.0.0 ([#272](https://github.com/dangoslen/changelog-enforcer/pull/272))
33 | - Bump `@actions/github` from 5.1.1 to 6.0.0 ([#273](https://github.com/dangoslen/changelog-enforcer/pull/273))
34 | - Bump `@actions/core` from 1.10.0 to 1.10.1 ([#274](https://github.com/dangoslen/changelog-enforcer/pull/274))
35 | - Bump `eslint` from 8.46.0 to 8.56.0 ([#279](https://github.com/dangoslen/changelog-enforcer/pull/279))
36 |
37 | ## [v3.5.1]
38 | ### Security
39 | - Removes `uglify-js` and `dist` packages
40 |
41 | ### Dependencies
42 | - Bump `jest` from 29.5.0 to 29.6.2 ([#260](https://github.com/dangoslen/changelog-enforcer/pull/260))
43 | - Bump `eslint` from 8.42.0 to 8.46.0 ([#261](https://github.com/dangoslen/changelog-enforcer/pull/261))
44 |
45 | ## [v3.5.0]
46 | ### Dependencies
47 | - Bump `@vercel/ncc` from 0.34.0 to 0.36.1 (#247)
48 | - Bump `eslint` from 8.31.0 to 8.42.0 (#249)
49 | - Bump `actions/checkout` from 3.5.2 to 3.5.3 (#250)
50 | - Bump `node-fetch` from 2.6.9 to 2.6.12 (#251, #253)
51 |
52 | ### Fixed
53 | - Handle `skipLabels` that contain a `/` (#254)
54 |
55 | ## [v3.4.0]
56 | ### Changed
57 | - Switches the default branch from `master` to `main`
58 |
59 | ### Dependencies
60 | - Bump `actions/checkout` from 3.2.0 to 3.5.2 (#245)
61 | - Bump `jest` from 29.3.1 to 29.5.0 (#242)
62 | - Bump `node-fetch` from 2.6.7 to 2.6.9 (#241)
63 |
64 | ## [v3.3.2]
65 | ### Fixed
66 | - Properly rebuilds the `dist.index.js` meant to be built in `v3.3.1`.
67 |
68 | ## [v3.3.1] - YANKED
69 |
70 | _This release has been yanked and should not be used. Please use `v3.3.2` instead. The tag for this release will be deleted on `2023-06-01` and will not be usable after that date. If you are using the `v3` tag, you will get the latest version automatically._
71 |
72 | ### Fixed
73 | - Removes the deprecated `set-output` command by bumping `@actions/core`. This fixes [issue #222](https://github.com/dangoslen/changelog-enforcer/issues/222)
74 |
75 | ### Dependencies
76 | - Bumps `@vercel/ncc` from 0.33.4 to 0.34.0
77 | - Bumps `stefanzweifel/git-auto-commit-action` from 4.15.4 to 4.16.0
78 | - Bumps `jest` from 29.2.2 to 29.3.1
79 | - Bumps `actions/checkout` from 3.1.0 to 3.2.0
80 | - Bumps `@actions/github` from 5.0.2 to 5.1.1
81 | - Bumps `eslint` from 8.2.0 to 8.31.0
82 | - Bumps `dangoslen/dependabot-changelog-helper` from 2 to 3
83 | - Bumps `@actions/core` from 1.9.0 to 1.10.0
84 |
85 | ## [v3.3.0]
86 | ### Dependencies
87 | - Bumps `stefanzweifel/git-auto-commit-action` from 4.14.1 to 4.15.4
88 | - Bumps `actions/checkout` from 3.0.2 to 3.1.0
89 | - Bumps `@actions/core` from 1.6.0 to 1.9.0
90 | - Bumps `uglify-js` from 3.15.5 to 3.17.4
91 | - Bumps `jest` from 27.3.1 to 29.2.2
92 |
93 | ## [v3.2.1]
94 | ### Changed
95 | - `expectedLatestVersion` no longer enforces validation if the only version in the changelog is an unreleased version.
96 | - See more in the [README](./README.md#expectedlatestversion)
97 |
98 | ## [v3.2.0]
99 | ### Changed
100 | - Now runs on Node 16
101 | - Adds `.nvmrc` to set the version
102 | - Updates node version in `action.yml`
103 | ### Dependencies
104 | - Bumps `uglify-js` from 3.14.3 to 3.15.5
105 | - Bumps `@actions/github` from 5.0.0 to 5.0.2
106 | - Bumps `stefanzweifel/git-auto-commit-action` from 4.14.0 to 4.14.1
107 |
108 | ## [v3.1.0]
109 | ### Fixes
110 | - Fixes issue #184
111 | - Get changelog from the `contents_url` instead of the `raw_url`
112 | ### Dependencies
113 | - Bumps `actions/checkout` from 2.4.0 to 3.0.2
114 | - Bumps `stefanzweifel/git-auto-commit-action` from 4.13.1 to 4.14.0
115 | - Removed `@actions/exec`
116 | - Bumps `@vercel/ncc` from 0.31.1 to 0.33.4
117 |
118 | ## [v3.0.1]
119 | ### Dependencies
120 | - Bumps `stefanzweifel/git-auto-commit-action` from 4.11.0 to 4.13.1
121 | - Bumps `@vercel/ncc` from 0.31.1 to 0.33.4
122 |
123 | ## [v3.0.0]
124 | :rocket: The 3.0.0 release of the Changelog Enforcer is here! This release relies soley on the GitHub API instead of local git commands from a cloned repository. This means, for example, that `actions/checkout` does **not** need to be run before running the enforcer.
125 | ### Fixes
126 | - Fixes issue #142
127 | ### Dependencies
128 | - Bumps `@vercel/ncc` from 0.28.6 to 0.31.1
129 | - Bumps `@actions/core` from 1.4.0 to 1.6.0
130 | - Bumps `jest` from 27.0.5 to 27.3.1
131 | - Bumps `actions/checkout` from 2.3.4 to 2.4.0
132 | - Bumps `uglify-js` from 3.13.9 to 3.14.3
133 | - Bumps `eslint` from 7.28.0 to 8.2.0
134 |
135 | ## [v2.3.1]
136 | ### Changed
137 | - Only runs on `pull_request` and `pull_request_target` events. This is to address issue #140
138 |
139 | ## [v2.3.0]
140 | ### Dependencies
141 | - Bumps `lodash` from 4.17.19 to 4.17.21
142 | - Bumps `stefanzweifel/git-auto-commit-action` from 4 to 4.11.0
143 | - Bumps `actions/checkout` from 2 to 2.3.4
144 | - Bumps `actions/create-release` from 1 to 1.1.4
145 | - Bumps `uglify-js` from 3.13.3 to 3.13.9
146 | - Bumps `eslint` from 7.25.0 to 7.28.0
147 | - Bumps `@vercel/ncc` from 0.28.2 to 0.28.6
148 | - Bumps `@actions/github` from 4.0.0 to 5.0.0
149 | - Bumps `dangoslen/dependabot-changelog-helper` from 0.3.2 to 1
150 | - Bumps `@actions/exec` from 1.0.4 to 1.1.0
151 | - Bumps `@actions/core` from 1.2.7 to 1.4.0
152 | - Bumps `jest` from 26.6.3 to 27.0.5
153 | - Bumps `ws` from 7.4.0 to 7.5.3
154 |
155 | ## [v2.2.0]
156 | ### Changed
157 | - The `pull_request` workflow now executes as a `pull_request_target` workflow to handle incoming pull requests from forked repos.
158 | - This is needed because Dependabot now works as a [forked branch](https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/). The reasoning and ways to accommodate are listed in a [GitHub Security article](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)
159 | - The `verified` label is needed to allow the workflow to execute
160 | ### Dependencies
161 | - Bumps `uglify-js` from 3.13.2 to 3.13.3
162 | - Bumps `y18n` from 4.0.1 to 5.0.8
163 | - Bumps `@vercel/ncc` from 0.27.0 to 0.28.2
164 | - Bumps `@actions/core` from 1.2.6 to 1.2.7
165 | - Bumps `eslint` from 7.23.0 to 7.25.0
166 | - Bumps `hosted-git-info` from 2.8.8 to 2.8.9
167 |
168 | ## [v2.1.0]
169 | ### Deprecated
170 | - The input `versionPattern` is now deprecated. Starting in `v3.0.0` the Changelog Enforcer will only work with [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for verifying the latest expected version.
171 | ### Dependencies
172 | - Bumps `eslint` from 7.21.0 to 7.23.0
173 | - Bumps `uglify-js` from 3.13.0 3.13.2
174 |
175 | ## [v2.0.2]
176 | ### Changed
177 | - Minor changes to workflows to use `package.json` version
178 | - Minor changes to tests and names
179 | ### Dependencies
180 | - Bumps `uglify-js` from 3.12.1 to 3.13.0
181 | - Bumps `eslint` from 7.20.0 to 7.21.0
182 |
183 | ## [v2.0.1]
184 | ### Dependencies
185 | - Bump `eslint` from 7.17.0 to 7.20.0
186 | - Bump `@vercel/ncc` from 0.26.1 to 0.27.0
187 | ### Changed
188 | - Now reads the version from `package.json` instead of from `VERSION`
189 |
190 | ## [v2.0.0]
191 | ### Added
192 | - Feature request from #62
193 | - Adds a new property `missingUpdateErrorMessage` for passing a custom error message when no update is found to the changelog. See the [Inputs / Properties](https://github.com/dangoslen/changelog-enforcer#inputs--properties) section in the `README.md` for more information.
194 | - Adds a new output `errorMessage` that states why the Changelog Enforcer failed. Added to allow users to use the error message within the rest of the action workflow.
195 | ### Dependencies
196 | - Bumps `@vercel/ncc` from `0.25.1` to `0.26.1` (#63)
197 | - Bumps `eslint` from `7.15.0` to `7.17.0` (#64, #70)
198 | - Bumps `node-notifier` from `8.0.0` to `8.0.1` (#65)
199 |
200 | ## [v1.6.1]
201 | ### Fixed
202 | - Fixes #58 by properly accounting for whitespace characters in label names.
203 |
204 | ## [v1.6.0]
205 | ### Added
206 | - New `skipLabels` input variable to supply a list of labels to skip enforcement for. See the [Inputs / Properties](https://github.com/dangoslen/changelog-enforcer#inputs--properties) section in the `README.md` for more information.
207 | ### Changed
208 | - Deprecates the `skipLabel` input variable in favor of the `skipLabels` input variable
209 | ### Dependencies
210 | - `eslint` from `7.14.0` to `7.15.0`
211 | - `uglify-js` from `2.6.0` to `3.12.1`
212 | - `jest` from `24.9.0` to `26.6.3`
213 |
214 | ## [v1.5.1]
215 | ### Added
216 | - Improved GitHub actions workflow for testing and packaging
217 | - Preview of release notes for a new version
218 | ### Dependencies
219 | - `@actions/exec` from `1.0.3` to `1.0.4`
220 | - `@actions/github` from `2.1.1` to `4.0.0`
221 | - `eslint` from `6.3.0` to `7.14.0`
222 | - `changelog-reader-action` from `v1` to `v2`
223 |
224 | ## [v1.5.0]
225 | ### Added
226 | - New input parameter `expectedLatestVersion`.
227 | - When supplied, the Changelog Enforcer validates that this is the latest version in the changelog or the latest version after an "Unreleased" version if one exists.
228 | - New input parameter `versionPattern`.
229 | - Used in conjunction with `expectedLatestVersion`. This is a javascript string that is converted to a regular expression that is used to extract the versions in the changelog identified by the `changeLogPath` input. By default is uses a regular expression for the [KeepAChangelog.org format](https://keepachangelog.com/en/1.0.0/).
230 | ### Changed
231 | - Updates to `README` and `CHANGELOG` for new features
232 |
233 | ## [v1.4.1]
234 | ### Security
235 | - `@actions/core@1.1.1` to `@actions/core@1.2.6`
236 | ### Adds
237 | - Badge for workflows using this action
238 |
239 | ## [v1.4.0]
240 | ### Summary
241 | Please upgrade to use with `actions/checkout@v2`!
242 | ### Fixes
243 | - Now works with both `actions/checkout@v1` and `actions/checkout@v2`
244 | ### Adds
245 | - Code coverage checks via `jest` and coverage badge via `make-coverage-badge`
246 |
247 | ## [v1.3.0]
248 | ### Security
249 | - `node-fetch@2.6.0` to `node-fetch@2.6.1`
250 | - `yargs-parser@13.1.1` to `yargs-parser@13.1.2`
251 |
252 | ## [v1.2.0]
253 | ### Added
254 | - Automatically builds the distribution on pull requests if all tests and enforcement pass
255 |
256 | ### Updated
257 | - Small `README` updates
258 |
259 | ## [v1.1.2]
260 | ### Security
261 | - `lodash@4.17.15` to `lodash@4.17.19`
262 |
263 | ## [v1.1.1]
264 | ### Fixes
265 | - Referencing proper step id in workflow for creating releases
266 |
267 | ## [v1.1.0]
268 | ### Added
269 | - Using [Changelog Reader](https://github.com/marketplace/actions/changelog-reader) to automate creating GitHub Releases from this `CHANGELOG.md`
270 |
271 | ## [v1.0.2]
272 | ### Security
273 | - Update uglify-js to 2.6.0 per [CVE-2015-8857](https://github.com/advisories/GHSA-34r7-q49f-h37c)
274 |
275 | ## [v1.0.1]
276 | ### Fixed
277 | - Fixes spelling of `skipLabel` property in `README.md`
278 |
279 | ## [v1.0.0]
280 | ### Added
281 | - Adds updates to the `README.md` and `action.yaml` to prepare to the GitHub marketplace
282 |
283 | ## [v0.1.0]
284 | - Initial `Changelog Enforcer` functionality, including the use of a label to skip
285 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 GitHub Actions
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 |
2 |
3 |
4 |
5 |
6 |
7 | ## Changelog Enforcer
8 |
9 | The purpose of this action is to enforce that every pull request in a repository includes a change to an ongoing changelog file. Inspired by [KeepAChangelog](https://keepachangelog.com/en/1.0.0/), this action helps development teams to keep a change file up to date as new features or fixes are implemented.
10 |
11 | ### Usage
12 |
13 | To use this action, follow the typical GitHub Action `uses` syntax. An example workflow using the default parameters of this action is shown below:
14 |
15 | ```yaml
16 | name: "Pull Request Workflow"
17 | on:
18 | pull_request:
19 | # The specific activity types are listed here to include "labeled" and "unlabeled"
20 | # (which are not included by default for the "pull_request" trigger).
21 | # This is needed to allow skipping enforcement of the changelog in PRs with specific labels,
22 | # as defined in the (optional) "skipLabels" property.
23 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
24 |
25 | jobs:
26 | # Enforces the update of a changelog file on every pull request
27 | changelog:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: dangoslen/changelog-enforcer@v3
31 | ```
32 |
33 | Other examples can be seen in the [example-workflows](./example-workflows) directory in this repository.
34 |
35 | _:warning: The Changelog Enforcer is designed to be used with the `pull_request` or `pull_request_target` event types. Using this action on any other event type will result in a warning logged and the action succeeding (as to not block the rest of a workflow)._
36 |
37 | ### Inputs / Properties
38 |
39 | Below are the properties allowed by the Changelog Enforcer. These properties are shipped with sane defaults for typical use, especially for changelogs inline with the [KeepAChangelog](Keepachangelog.org) format.
40 |
41 | #### `changeLogPath`
42 | * Default: `CHANGELOG.md`
43 | * The path to your changelog file. Should be from the perspective of the root directory to `git`. The file being checked for updates must be either an add (`A`) or modified (`M`) status to `git` to qualify as updated.
44 |
45 | #### `skipLabels`
46 | * Default: `'Skip-Changelog'`
47 | * List of labels used to skip enforcing of the changelog during a pull request. Each label name is comma separated and only one label needs to be present for enforcement to be skipped.
48 |
49 | For example, if `label-1,label-2` was supplied as the `skipLabels`, `label-1` _or_ `label-2` would skip the enforcer. Each label is trimmed for leading and trailing spaces since GitHub labels do not allow for leading or trailing spaces. Thus, the following lists are equivalent:
50 | * `label-1,label-2`
51 | * `label-1 , label-2`
52 | * `label-1 ,label-2`
53 |
54 | #### `missingUpdateErrorMessage`
55 | * Default: `''`
56 | * Custom error message to use when no update to the changelog is found.
57 |
58 | #### `expectedLatestVersion`
59 | * Default: `''`
60 | * The latest version of the software expected in the changelog. Should be in the form of `v1.1.0`, `v3.5.6` etc. Allows for the first version in the changelog to be an unreleased version (either `unreleased|Unreleased|UNRELEASED`) before checking versions. If the only version in the changelog is an unreleased version, no validation occurs. This is to support a repository adding a changelog after other versions have been released and don't want to backport previous versions (though doing so is recommended).
61 |
62 | #### `versionPattern`
63 | * Default: `'## \\[((v|V)?\\d*\\.\\d*\\.\\d*-?\\w*|unreleased|Unreleased|UNRELEASED)\\]'`
64 | * A regex pattern used to extract the version section headings from the changelog. The Changelog Enforcer assumes the use of the [KeepAChangelog.com](https://keepachangelog.com/en/1.0.0/) convention for section headings, and as such looks for a line starting with `## [version] - date`. Versions are only extracted from the changelog when enforcing the expected latest version (via the `expectedLatestVersion` property).
65 |
66 | If you supply your own regex to match a different format, your regex must match the version string as a capture group (in the default format, that's the part inside square brackets). The first capture group will be used if your regex includes multiple groups. The regex pattern is used with global and multiline flags to find all of the versions in the changelog.
67 |
68 | Because the regex is passed as a `String` object, you will need to escape backslash characters (`\`) via `\\`.
69 |
70 | #### `token`
71 | * Default: `${{ github.token }}`
72 | * The token used to authenticate to the GitHub API. Uses the default token from the `github.token` context. Can be any access token you have configured for your repository.
73 |
74 | ### Outputs
75 |
76 | #### `errorMessage`
77 | * The reason for why the Changelog Enforcer failed. Uses the `missingUpdateErrorMessage` property value if set when no update to the changelog is found.
78 |
79 | ### Creating Releases Automatically
80 |
81 | Using this Action and the [Changelog Reader](https://github.com/mindsers/changelog-reader-action), plus a few standard GitHub created Actions, we can keep the changelog of a project up to date and create a GitHub release automatically with contents from the changelog. See this project's [release.yml](./.github/workflows/release.yml) for how to set up a simple workflow to create a new release based on a `VERSION` file and a changelog.
82 |
--------------------------------------------------------------------------------
/__tests__/changelog-enforcer.test.js:
--------------------------------------------------------------------------------
1 | jest.mock('node-fetch');
2 |
3 | const core = require('@actions/core')
4 | const fetch = require('node-fetch')
5 | const { Response } = jest.requireActual('node-fetch');
6 | const changelogEnforcer = require('../src/changelog-enforcer')
7 |
8 | const SKIP_LABELS = "SomeLabel,Skip-Changelog,Skip-Release"
9 | const CHANGELOG = "CHANGELOG.md"
10 | const VERSION_PATTERN = "^## \\[((v|V)?\\d*\\.\\d*\\.\\d*-?\\w*|unreleased|Unreleased|UNRELEASED)\\]"
11 |
12 | // Inputs for mock @actions/core
13 | let inputs = {}
14 |
15 | // Mocks via Jest
16 | let infoSpy
17 | let failureSpy
18 | let outputSpy
19 |
20 | describe('the changelog-enforcer', () => {
21 |
22 | afterAll(() => {
23 | jest.restoreAllMocks()
24 | })
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks()
28 |
29 | inputs['skipLabels'] = SKIP_LABELS
30 | inputs['changeLogPath'] = CHANGELOG
31 | inputs['expectedLatestVersion'] = ''
32 | inputs['versionPattern'] = VERSION_PATTERN
33 | inputs['token'] = 'token'
34 |
35 | jest.spyOn(core, 'getInput').mockImplementation((name) => {
36 | return inputs[name]
37 | })
38 |
39 | octokit = {}
40 |
41 | infoSpy = jest.spyOn(core, 'info').mockImplementation(jest.fn())
42 | failureSpy = jest.spyOn(core, 'setFailed').mockImplementation(jest.fn())
43 | outputSpy = jest.spyOn(core, 'setOutput').mockImplementation(jest.fn())
44 | })
45 |
46 | prepareResponse = (body) => {
47 | return Promise.resolve(new Response(body, { Headers: { 'Content-Type': 'application/json' } }))
48 | }
49 |
50 | it('should skip enforcing when label is present', (done) => {
51 | changelogEnforcer.enforce()
52 | .then(() => {
53 | expect(infoSpy).toHaveBeenCalledTimes(5)
54 | expect(failureSpy).not.toHaveBeenCalled()
55 | expect(outputSpy).not.toHaveBeenCalled()
56 |
57 | done()
58 | })
59 | })
60 |
61 | it('should throw an error when token is missing', (done) => {
62 | inputs['token'] = ''
63 |
64 | changelogEnforcer.enforce()
65 | .then(() => {
66 | expect(infoSpy).not.toHaveBeenCalled()
67 | expect(failureSpy).toHaveBeenCalled()
68 | expect(outputSpy).toHaveBeenCalled()
69 |
70 | done()
71 | })
72 | })
73 |
74 | it('should enforce when label is not present; changelog is changed', (done) => {
75 | inputs['skipLabels'] = 'A different label'
76 |
77 | const files = [
78 | {
79 | "filename": "CHANGELOG.md",
80 | "status": "modified",
81 | "contents_url": "./path/to/CHANGELOG.md"
82 | }
83 | ]
84 |
85 | fetch.mockImplementation((url, options) => {
86 | return prepareResponse(JSON.stringify(files))
87 | })
88 |
89 | changelogEnforcer.enforce()
90 | .then(() => {
91 | expect(infoSpy).toHaveBeenCalledTimes(5)
92 | expect(failureSpy).not.toHaveBeenCalled()
93 | expect(outputSpy).not.toHaveBeenCalled()
94 |
95 | expect(fetch).toHaveBeenCalledTimes(1)
96 |
97 | done()
98 | })
99 | })
100 |
101 | it('should enforce when label is not present; changelog is not changed', (done) => {
102 | inputs['skipLabels'] = 'A different label'
103 |
104 | const files = [
105 | {
106 | "filename": "AnotherFile.md",
107 | "status": "modified",
108 | "contents_url": "/path/to/AnotherFile.md"
109 | }
110 | ]
111 |
112 |
113 | fetch.mockImplementation((url, options) => {
114 | return prepareResponse(JSON.stringify(files))
115 | })
116 |
117 | changelogEnforcer.enforce()
118 | .then(() => {
119 | expect(infoSpy).toHaveBeenCalledTimes(5)
120 | expect(failureSpy).toHaveBeenCalled()
121 | expect(outputSpy).toHaveBeenCalled()
122 |
123 | expect(fetch).toHaveBeenCalledTimes(1)
124 |
125 | done()
126 | })
127 | })
128 |
129 | it('should enforce when label is not present; changelog is not changed; custom error message', (done) => {
130 | const customErrorMessage = 'Some Message for you @Author!'
131 | inputs['skipLabels'] = 'A different label'
132 | inputs['missingUpdateErrorMessage'] = customErrorMessage
133 |
134 | const files = [
135 | {
136 | "filename": "AnotherFile.md",
137 | "status": "modified",
138 | "contents_url": "/path/to/AnotherFile.md"
139 | }
140 | ]
141 |
142 | fetch.mockImplementation((url, options) => {
143 | return prepareResponse(JSON.stringify(files))
144 | })
145 |
146 | changelogEnforcer.enforce()
147 | .then(() => {
148 | expect(infoSpy).toHaveBeenCalledTimes(5)
149 | expect(failureSpy).toHaveBeenCalled()
150 | expect(outputSpy).toHaveBeenCalledWith('errorMessage', customErrorMessage)
151 |
152 | expect(fetch).toHaveBeenCalledTimes(1)
153 |
154 | done()
155 | })
156 | })
157 |
158 | it('should enforce when label is not present; changelog is changed; versions do not match', (done) => {
159 | const contentsUrl = 'some-url'
160 | inputs['skipLabels'] = 'A different label'
161 | inputs['expectedLatestVersion'] = 'v2.0.0'
162 |
163 | const files = [
164 | {
165 | "filename": "CHANGELOG.md",
166 | "status": "modified",
167 | "contents_url": contentsUrl
168 | }
169 | ]
170 |
171 | const changelog =
172 | `## [v2.1.0]
173 | - Changelog
174 | `
175 |
176 | fetch.mockImplementation((url, options) => {
177 | if (url === contentsUrl) {
178 | return Promise.resolve(new Response(changelog))
179 | }
180 | return prepareResponse(JSON.stringify(files))
181 | })
182 |
183 | changelogEnforcer.enforce()
184 | .then(() => {
185 | expect(infoSpy).toHaveBeenCalledTimes(5)
186 | expect(failureSpy).toHaveBeenCalled()
187 | expect(outputSpy).toHaveBeenCalled()
188 |
189 | expect(fetch).toHaveBeenCalledTimes(2)
190 |
191 | done()
192 | })
193 | })
194 |
195 | it('should enforce when label is not present; changelog is changed; only one unreleased version exists', (done) => {
196 | const contentsUrl = 'some-url'
197 | inputs['skipLabels'] = 'A different label'
198 | inputs['expectedLatestVersion'] = 'v2.0.0'
199 |
200 | const files = [
201 | {
202 | "filename": "CHANGELOG.md",
203 | "status": "modified",
204 | "contents_url": contentsUrl
205 | }
206 | ]
207 |
208 | const changelog =
209 | `## [Unreleased]
210 | - Changelog
211 | `
212 |
213 | fetch.mockImplementation((url, options) => {
214 | if (url === contentsUrl) {
215 | return Promise.resolve(new Response(changelog))
216 | }
217 | return prepareResponse(JSON.stringify(files))
218 | })
219 |
220 | changelogEnforcer.enforce()
221 | .then(() => {
222 | expect(infoSpy).toHaveBeenCalledTimes(5)
223 | expect(failureSpy).not.toHaveBeenCalled()
224 | expect(outputSpy).not.toHaveBeenCalled()
225 |
226 | expect(fetch).toHaveBeenCalledTimes(2)
227 |
228 | done()
229 | })
230 | })
231 | })
--------------------------------------------------------------------------------
/__tests__/client.test.js:
--------------------------------------------------------------------------------
1 | jest.mock('node-fetch');
2 |
3 | const fetch = require('node-fetch')
4 | const { Response } = jest.requireActual('node-fetch');
5 | const client = require('../src/client')
6 |
7 | describe('the client', () => {
8 |
9 | afterAll(() => {
10 | jest.restoreAllMocks()
11 | })
12 |
13 | beforeEach(() => {
14 | jest.clearAllMocks()
15 | })
16 |
17 | prepareResponse = (body) => {
18 | return Promise.resolve(new Response(body, { Headers: { 'Content-Type': 'application/json' } }))
19 | }
20 |
21 | it('should find the change file', async () => {
22 | const files = [
23 | {
24 | "filename": "CHANGELOG.md",
25 | "status": "modified",
26 | "contents_url": "./path/to/CHANGELOG.md"
27 | }
28 | ]
29 |
30 | fetch.mockReturnValueOnce(prepareResponse(JSON.stringify(files)))
31 |
32 | const changelogFile = await client.findChangelog('token', 'repo', 1, 1, 'CHANGELOG.md')
33 | expect(fetch).toHaveBeenCalled()
34 | expect(changelogFile).toStrictEqual({
35 | "filename": "CHANGELOG.md",
36 | "status": "modified",
37 | "contents_url": "./path/to/CHANGELOG.md"
38 | })
39 | })
40 |
41 | it('should not find the change file', async () => {
42 | const firstPage = [
43 | {
44 | "filename": "random.md",
45 | "status": "modified",
46 | "contents_url": "./path/to/random.md"
47 | }
48 | ]
49 |
50 | const secondPage = []
51 |
52 | fetch
53 | .mockReturnValueOnce(prepareResponse(JSON.stringify(firstPage)))
54 | .mockReturnValueOnce(prepareResponse(JSON.stringify(secondPage)))
55 |
56 | const changelogFile = await client.findChangelog('token', 'repo', 1, 1, 'CHANGELOG.md')
57 | expect(fetch).toHaveBeenCalledTimes(2)
58 | expect(changelogFile).toBeUndefined()
59 | })
60 |
61 | it('should get an error with bad response code', async () => {
62 | fetch
63 | .mockReturnValueOnce(Promise.resolve(new Response("", { status: 401 })))
64 |
65 | try {
66 | await client.findChangelog('token', 'repo', 1, 1, 'CHANGELOG.md')
67 | } catch (err) {
68 | expect(fetch).toHaveBeenCalled()
69 | }
70 | })
71 | })
--------------------------------------------------------------------------------
/__tests__/context-extractor.test.js:
--------------------------------------------------------------------------------
1 | const contextExtractor = require('../src/context-extractor')
2 | const core = require('@actions/core')
3 |
4 | const PULL = {
5 | key: 'value'
6 | }
7 |
8 | const CONTEXT_PULL = {
9 | eventName: 'pull_request',
10 | payload: {
11 | pull_request: PULL
12 | }
13 | }
14 |
15 | const CONTEXT_PUSH = {
16 | eventName: 'push',
17 | payload: {}
18 | }
19 |
20 | let warnSpy;
21 |
22 | describe('the context-extractor', () => {
23 |
24 | afterAll(() => {
25 | jest.restoreAllMocks()
26 | })
27 |
28 | beforeEach(() => {
29 | jest.clearAllMocks()
30 |
31 | warnSpy = jest.spyOn(core, 'warning').mockImplementation(jest.fn())
32 | })
33 |
34 | it('will return the pull request context', () => {
35 | const pull = contextExtractor.getPullRequestContext(CONTEXT_PULL)
36 |
37 | expect(pull).toBe(PULL)
38 | })
39 |
40 | it('will error if not pull request context', () => {
41 | const cntxt = contextExtractor.getPullRequestContext(CONTEXT_PUSH)
42 |
43 | expect(cntxt).toBe(undefined)
44 | expect(warnSpy.mock.calls.length).toBe(1)
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/__tests__/env.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const eventPath = path.resolve(__dirname, 'test_pull_request.json')
3 | process.env.GITHUB_EVENT_PATH = eventPath
4 |
--------------------------------------------------------------------------------
/__tests__/label-extractor.test.js:
--------------------------------------------------------------------------------
1 | const labelExtractor = require('../src/label-extractor')
2 |
3 | const EXPECTED_LABELS = ['label-1','label-2_with_underscore','special-[characters](please)']
4 | const EXPECTED_LABELS_SPACES = ['label 1','label 2_with_underscore','special [characters] (please)']
5 | const EXPECTED_SINGLE_ENTRY = ['no changelog entry needed']
6 |
7 | describe('the label-extractor', () => {
8 |
9 | it('should return all labels', () => {
10 | const labels = labelExtractor.extractLabels('label-1,label-2_with_underscore,special-[characters](please)')
11 |
12 | expect(labels).toStrictEqual(EXPECTED_LABELS)
13 | })
14 |
15 | it('should return all labels when spaces are included and trailing comma', () => {
16 | const labels = labelExtractor.extractLabels('label-1 , label-2_with_underscore , special-[characters](please),')
17 |
18 | expect(labels).toStrictEqual(EXPECTED_LABELS)
19 | })
20 |
21 | it('should return all labels with spaces', () => {
22 | const labels = labelExtractor.extractLabels('label 1,label 2_with_underscore,special [characters] (please)')
23 |
24 | expect(labels).toStrictEqual(EXPECTED_LABELS_SPACES)
25 | })
26 |
27 | it('should return only a single labels with spaces', () => {
28 | const labels = labelExtractor.extractLabels('no changelog entry needed')
29 |
30 | expect(labels).toStrictEqual(EXPECTED_SINGLE_ENTRY)
31 | })
32 |
33 | it('should handle labels containing a forward slash', () => {
34 | const labels = labelExtractor.extractLabels('skip/changelog')
35 |
36 | expect(labels).toStrictEqual(['skip/changelog'])
37 | })
38 |
39 | it('should handle multiple labels containing a forward slash', () => {
40 | const labels = labelExtractor.extractLabels('skip/changelog,no/changelog')
41 |
42 | expect(labels).toStrictEqual(['skip/changelog', 'no/changelog'])
43 | })
44 |
45 |
46 | it('should handle multiple labels containing a `:` characters (emoji usage)', () => {
47 | const labels = labelExtractor.extractLabels(':wrench: GitHub Actions, :smile: Best Label Ever')
48 |
49 | expect(labels).toStrictEqual([':wrench: GitHub Actions', ':smile: Best Label Ever'])
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/__tests__/test_pull_request.json:
--------------------------------------------------------------------------------
1 | {
2 | "action": "opened",
3 | "number": 2,
4 | "pull_request": {
5 | "url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2",
6 | "id": 279147437,
7 | "node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3",
8 | "html_url": "https://github.com/Codertocat/Hello-World/pull/2",
9 | "diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff",
10 | "patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch",
11 | "issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2",
12 | "number": 2,
13 | "state": "open",
14 | "locked": false,
15 | "title": "Update the README with new information.",
16 | "user": {
17 | "login": "Codertocat",
18 | "id": 21031067,
19 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
20 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
21 | "gravatar_id": "",
22 | "url": "https://api.github.com/users/Codertocat",
23 | "html_url": "https://github.com/Codertocat",
24 | "followers_url": "https://api.github.com/users/Codertocat/followers",
25 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
26 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
27 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
28 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
29 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
30 | "repos_url": "https://api.github.com/users/Codertocat/repos",
31 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
32 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
33 | "type": "User",
34 | "site_admin": false
35 | },
36 | "body": "This is a pretty simple change that we need to pull into master.",
37 | "created_at": "2019-05-15T15:20:33Z",
38 | "updated_at": "2019-05-15T15:20:33Z",
39 | "closed_at": null,
40 | "merged_at": null,
41 | "merge_commit_sha": null,
42 | "assignee": null,
43 | "assignees": [
44 |
45 | ],
46 | "requested_reviewers": [
47 |
48 | ],
49 | "requested_teams": [
50 |
51 | ],
52 | "labels": [
53 | { "name": "Skip-Changelog" },
54 | { "name": "bug" }
55 | ],
56 | "milestone": null,
57 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits",
58 | "review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments",
59 | "review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}",
60 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments",
61 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821",
62 | "head": {
63 | "label": "Codertocat:changes",
64 | "ref": "changes",
65 | "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821",
66 | "user": {
67 | "login": "Codertocat",
68 | "id": 21031067,
69 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
70 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
71 | "gravatar_id": "",
72 | "url": "https://api.github.com/users/Codertocat",
73 | "html_url": "https://github.com/Codertocat",
74 | "followers_url": "https://api.github.com/users/Codertocat/followers",
75 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
76 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
77 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
78 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
79 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
80 | "repos_url": "https://api.github.com/users/Codertocat/repos",
81 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
82 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
83 | "type": "User",
84 | "site_admin": false
85 | },
86 | "repo": {
87 | "id": 186853002,
88 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
89 | "name": "Hello-World",
90 | "full_name": "Codertocat/Hello-World",
91 | "private": false,
92 | "owner": {
93 | "login": "Codertocat",
94 | "id": 21031067,
95 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
96 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
97 | "gravatar_id": "",
98 | "url": "https://api.github.com/users/Codertocat",
99 | "html_url": "https://github.com/Codertocat",
100 | "followers_url": "https://api.github.com/users/Codertocat/followers",
101 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
102 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
103 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
104 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
105 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
106 | "repos_url": "https://api.github.com/users/Codertocat/repos",
107 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
108 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
109 | "type": "User",
110 | "site_admin": false
111 | },
112 | "html_url": "https://github.com/Codertocat/Hello-World",
113 | "description": null,
114 | "fork": false,
115 | "url": "https://api.github.com/repos/Codertocat/Hello-World",
116 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
117 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
118 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
119 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
120 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
121 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
122 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
123 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
124 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
125 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
126 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
127 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
128 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
129 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
130 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
131 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
132 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
133 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
134 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
135 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
136 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
137 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
138 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
139 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
140 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
141 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
142 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
143 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
144 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
145 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
146 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
147 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
148 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
149 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
150 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
151 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
152 | "created_at": "2019-05-15T15:19:25Z",
153 | "updated_at": "2019-05-15T15:19:27Z",
154 | "pushed_at": "2019-05-15T15:20:32Z",
155 | "git_url": "git://github.com/Codertocat/Hello-World.git",
156 | "ssh_url": "git@github.com:Codertocat/Hello-World.git",
157 | "clone_url": "https://github.com/Codertocat/Hello-World.git",
158 | "svn_url": "https://github.com/Codertocat/Hello-World",
159 | "homepage": null,
160 | "size": 0,
161 | "stargazers_count": 0,
162 | "watchers_count": 0,
163 | "language": null,
164 | "has_issues": true,
165 | "has_projects": true,
166 | "has_downloads": true,
167 | "has_wiki": true,
168 | "has_pages": true,
169 | "forks_count": 0,
170 | "mirror_url": null,
171 | "archived": false,
172 | "disabled": false,
173 | "open_issues_count": 2,
174 | "license": null,
175 | "forks": 0,
176 | "open_issues": 2,
177 | "watchers": 0,
178 | "default_branch": "master"
179 | }
180 | },
181 | "base": {
182 | "label": "Codertocat:master",
183 | "ref": "master",
184 | "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e",
185 | "user": {
186 | "login": "Codertocat",
187 | "id": 21031067,
188 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
189 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
190 | "gravatar_id": "",
191 | "url": "https://api.github.com/users/Codertocat",
192 | "html_url": "https://github.com/Codertocat",
193 | "followers_url": "https://api.github.com/users/Codertocat/followers",
194 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
195 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
196 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
197 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
198 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
199 | "repos_url": "https://api.github.com/users/Codertocat/repos",
200 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
201 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
202 | "type": "User",
203 | "site_admin": false
204 | },
205 | "repo": {
206 | "id": 186853002,
207 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
208 | "name": "Hello-World",
209 | "full_name": "Codertocat/Hello-World",
210 | "private": false,
211 | "owner": {
212 | "login": "Codertocat",
213 | "id": 21031067,
214 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
215 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
216 | "gravatar_id": "",
217 | "url": "https://api.github.com/users/Codertocat",
218 | "html_url": "https://github.com/Codertocat",
219 | "followers_url": "https://api.github.com/users/Codertocat/followers",
220 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
221 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
222 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
223 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
224 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
225 | "repos_url": "https://api.github.com/users/Codertocat/repos",
226 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
227 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
228 | "type": "User",
229 | "site_admin": false
230 | },
231 | "html_url": "https://github.com/Codertocat/Hello-World",
232 | "description": null,
233 | "fork": false,
234 | "url": "https://api.github.com/repos/Codertocat/Hello-World",
235 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
236 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
237 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
238 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
239 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
240 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
241 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
242 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
243 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
244 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
245 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
246 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
247 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
248 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
249 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
250 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
251 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
252 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
253 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
254 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
255 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
256 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
257 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
258 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
259 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
260 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
261 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
262 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
263 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
264 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
265 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
266 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
267 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
268 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
269 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
270 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
271 | "created_at": "2019-05-15T15:19:25Z",
272 | "updated_at": "2019-05-15T15:19:27Z",
273 | "pushed_at": "2019-05-15T15:20:32Z",
274 | "git_url": "git://github.com/Codertocat/Hello-World.git",
275 | "ssh_url": "git@github.com:Codertocat/Hello-World.git",
276 | "clone_url": "https://github.com/Codertocat/Hello-World.git",
277 | "svn_url": "https://github.com/Codertocat/Hello-World",
278 | "homepage": null,
279 | "size": 0,
280 | "stargazers_count": 0,
281 | "watchers_count": 0,
282 | "language": null,
283 | "has_issues": true,
284 | "has_projects": true,
285 | "has_downloads": true,
286 | "has_wiki": true,
287 | "has_pages": true,
288 | "forks_count": 0,
289 | "mirror_url": null,
290 | "archived": false,
291 | "disabled": false,
292 | "open_issues_count": 2,
293 | "license": null,
294 | "forks": 0,
295 | "open_issues": 2,
296 | "watchers": 0,
297 | "default_branch": "master"
298 | }
299 | },
300 | "_links": {
301 | "self": {
302 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2"
303 | },
304 | "html": {
305 | "href": "https://github.com/Codertocat/Hello-World/pull/2"
306 | },
307 | "issue": {
308 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2"
309 | },
310 | "comments": {
311 | "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments"
312 | },
313 | "review_comments": {
314 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments"
315 | },
316 | "review_comment": {
317 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}"
318 | },
319 | "commits": {
320 | "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits"
321 | },
322 | "statuses": {
323 | "href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821"
324 | }
325 | },
326 | "author_association": "OWNER",
327 | "draft": false,
328 | "merged": false,
329 | "mergeable": null,
330 | "rebaseable": null,
331 | "mergeable_state": "unknown",
332 | "merged_by": null,
333 | "comments": 0,
334 | "review_comments": 0,
335 | "maintainer_can_modify": false,
336 | "commits": 1,
337 | "additions": 1,
338 | "deletions": 1,
339 | "changed_files": 1
340 | },
341 | "repository": {
342 | "id": 186853002,
343 | "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=",
344 | "name": "Hello-World",
345 | "full_name": "Codertocat/Hello-World",
346 | "private": false,
347 | "owner": {
348 | "login": "Codertocat",
349 | "id": 21031067,
350 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
351 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
352 | "gravatar_id": "",
353 | "url": "https://api.github.com/users/Codertocat",
354 | "html_url": "https://github.com/Codertocat",
355 | "followers_url": "https://api.github.com/users/Codertocat/followers",
356 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
357 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
358 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
359 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
360 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
361 | "repos_url": "https://api.github.com/users/Codertocat/repos",
362 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
363 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
364 | "type": "User",
365 | "site_admin": false
366 | },
367 | "html_url": "https://github.com/Codertocat/Hello-World",
368 | "description": null,
369 | "fork": false,
370 | "url": "https://api.github.com/repos/Codertocat/Hello-World",
371 | "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks",
372 | "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}",
373 | "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}",
374 | "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams",
375 | "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks",
376 | "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}",
377 | "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events",
378 | "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}",
379 | "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}",
380 | "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags",
381 | "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}",
382 | "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}",
383 | "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}",
384 | "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}",
385 | "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}",
386 | "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages",
387 | "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers",
388 | "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors",
389 | "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers",
390 | "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription",
391 | "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}",
392 | "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}",
393 | "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}",
394 | "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}",
395 | "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}",
396 | "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}",
397 | "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges",
398 | "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}",
399 | "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads",
400 | "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}",
401 | "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}",
402 | "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}",
403 | "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}",
404 | "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}",
405 | "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}",
406 | "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments",
407 | "created_at": "2019-05-15T15:19:25Z",
408 | "updated_at": "2019-05-15T15:19:27Z",
409 | "pushed_at": "2019-05-15T15:20:32Z",
410 | "git_url": "git://github.com/Codertocat/Hello-World.git",
411 | "ssh_url": "git@github.com:Codertocat/Hello-World.git",
412 | "clone_url": "https://github.com/Codertocat/Hello-World.git",
413 | "svn_url": "https://github.com/Codertocat/Hello-World",
414 | "homepage": null,
415 | "size": 0,
416 | "stargazers_count": 0,
417 | "watchers_count": 0,
418 | "language": null,
419 | "has_issues": true,
420 | "has_projects": true,
421 | "has_downloads": true,
422 | "has_wiki": true,
423 | "has_pages": true,
424 | "forks_count": 0,
425 | "mirror_url": null,
426 | "archived": false,
427 | "disabled": false,
428 | "open_issues_count": 2,
429 | "license": null,
430 | "forks": 0,
431 | "open_issues": 2,
432 | "watchers": 0,
433 | "default_branch": "master"
434 | },
435 | "sender": {
436 | "login": "Codertocat",
437 | "id": 21031067,
438 | "node_id": "MDQ6VXNlcjIxMDMxMDY3",
439 | "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4",
440 | "gravatar_id": "",
441 | "url": "https://api.github.com/users/Codertocat",
442 | "html_url": "https://github.com/Codertocat",
443 | "followers_url": "https://api.github.com/users/Codertocat/followers",
444 | "following_url": "https://api.github.com/users/Codertocat/following{/other_user}",
445 | "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}",
446 | "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}",
447 | "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions",
448 | "organizations_url": "https://api.github.com/users/Codertocat/orgs",
449 | "repos_url": "https://api.github.com/users/Codertocat/repos",
450 | "events_url": "https://api.github.com/users/Codertocat/events{/privacy}",
451 | "received_events_url": "https://api.github.com/users/Codertocat/received_events",
452 | "type": "User",
453 | "site_admin": false
454 | }
455 | }
--------------------------------------------------------------------------------
/__tests__/version-extractor.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const versionExtractor = require('../src/version-extractor')
3 |
4 | const KEEP_A_CHANGELOG = `
5 | ## Unreleased
6 |
7 | ## [v1.10.0]
8 | - Some changes
9 |
10 | ## [v1.9.2]
11 | Fixed a bug
12 | `
13 |
14 | const CUSTOM = `
15 | * Unreleased
16 |
17 | * v1.2.0
18 | - Some changes
19 | `
20 | const VERSION_PATTERN = "^## \\[?((v|V)?\\d*\\.\\d*\\.\\d*-?\\w*|unreleased|Unreleased|UNRELEASED)\\]?"
21 | const CUSTOM_VERSION_PATTERN = "^\\* ((v|V)?\\d*\\.\\d*\\.\\d*-?\\w*|Unreleased)"
22 |
23 | describe('the verstion-extractor', () => {
24 |
25 | it('should return all versions via keep a changelog format', () => {
26 | const versions = versionExtractor.getVersions(VERSION_PATTERN, KEEP_A_CHANGELOG)
27 | expect(versions).toStrictEqual(['Unreleased', 'v1.10.0', 'v1.9.2'])
28 | })
29 |
30 | it('should return all versions via custom format', () => {
31 | const versions = versionExtractor.getVersions(CUSTOM_VERSION_PATTERN, CUSTOM)
32 | expect(versions).toStrictEqual(['Unreleased', 'v1.2.0'])
33 | })
34 |
35 | })
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Changelog Enforcer'
2 | author: '@dangoslen'
3 | description: 'Enforces a repository changelog to be kept up to date.'
4 | outputs:
5 | errorMessage:
6 | description: "A message containing the reason why the Changelog Enforcer failed."
7 | inputs:
8 | changeLogPath:
9 | description: 'Path to the changelog file relative to the repository'
10 | required: true
11 | default: 'CHANGELOG.md'
12 | skipLabels:
13 | description: |
14 | "List of labels used to skip enforcing of the changelog during a pull request. Each label name is comma separated and only one label needs to be
15 | present for enforcement to be skipped.
16 |
17 | For example, if `label-1,label-2` was supplied as the `skipLabels`, `label-1` _or_ `label-2` would skip the enforcer.
18 |
19 | Each label is trimmed for leading and trailing spaces since GitHub labels do not allow for leading or trailing spaces. Thus, the following lists are equivalent:
20 | * `label-1,label-2`
21 | * `label-1 , label-2`
22 | * `label-1 ,label-2`"
23 | required: true
24 | default: 'Skip-Changelog'
25 | expectedLatestVersion:
26 | description: "The latest version of the software expected in the changelog. Should be in the form of 'v1.1.0' etc."
27 | required: true
28 | default: ''
29 | versionPattern:
30 | description: |
31 | "A regex pattern used to extract the version section headings from the changelog. Changelog Enforcer assumes the use of the [KeepAChangelog.com](https://keepachangelog.com/en/1.0.0/) convention for section headings, and as such looks for a line starting with `## [version] - date`. Versions are only extracted from the changelog when enforcing the expected latest version (via the `expectedLatestVersion` property).
32 |
33 | If you supply your own regex to match a different format, your regex must match the version string as a capture group (in the default format, that's the part inside square brackets). The first capture group will be used if your regex includes multiple groups. The regex pattern is used with global and multiline flags to find all of the versions in the changelog.
34 |
35 | Because the regex is passed as a `String` object, you will need to escape backslash characters (`\`) via `\\`."
36 | required: true
37 | default: "^## \\[((v|V)?\\d*\\.\\d*\\.\\d*-?\\w*|unreleased|Unreleased|UNRELEASED)\\]"
38 | missingUpdateErrorMessage:
39 | description: "The error message logged and returned in the 'errorMessage' output when no update to the changelog has been found."
40 | required: false
41 | token:
42 | description: "The secret value from your GITHUB_TOKEN or another token to access the GitHub API. Defaults to the token at `github.token`"
43 | required: true
44 | default: ${{ github.token }}
45 | runs:
46 | using: 'node20'
47 | main: 'dist/index.js'
48 | branding:
49 | icon: 'check-square'
50 | color: 'orange'
51 |
--------------------------------------------------------------------------------
/bin/cut-release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | VERSION=$1
4 |
5 | # Bump package.json
6 | jq --arg VERSION $VERSION '.version = $VERSION' package.json > updated.json && mv updated.json package.json
7 |
8 | # Install latest deps
9 | npm install
10 |
11 | # Run all tests and package the dist/*
12 | npm run all
--------------------------------------------------------------------------------
/coverage/badge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import path from "node:path";
3 | import { fileURLToPath } from "node:url";
4 | import js from "@eslint/js";
5 | import { FlatCompat } from "@eslint/eslintrc";
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 | const compat = new FlatCompat({
10 | baseDirectory: __dirname,
11 | recommendedConfig: js.configs.recommended,
12 | allConfig: js.configs.all
13 | });
14 |
15 | export default [...compat.extends("eslint:recommended"), {
16 | languageOptions: {
17 | globals: {
18 | ...globals.commonjs,
19 | ...globals.node,
20 | Atomics: "readonly",
21 | SharedArrayBuffer: "readonly",
22 | },
23 |
24 | ecmaVersion: 2018,
25 | sourceType: "commonjs",
26 | },
27 |
28 | rules: {},
29 | }];
--------------------------------------------------------------------------------
/example-workflows/with-different-changelog-path.yaml:
--------------------------------------------------------------------------------
1 | ```yaml
2 | name: "Different Changelog Path"
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
6 |
7 | jobs:
8 | # Enforces the update of a changelog file on every pull request
9 | changelog:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: dangoslen/changelog-enforcer@v2
14 | with:
15 | changeLogPath: 'different/changelog.md'
16 | ```
--------------------------------------------------------------------------------
/example-workflows/with-different-token.yaml:
--------------------------------------------------------------------------------
1 | ```yaml
2 | name: "Different Token"
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
6 |
7 | jobs:
8 | # Enforces the update of a changelog file on every pull request
9 | changelog:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: dangoslen/changelog-enforcer@v2
14 | with:
15 | token: ${{ secrets. }}
16 | ```
--------------------------------------------------------------------------------
/example-workflows/with-expected-latest-version-custom-pattern.yaml:
--------------------------------------------------------------------------------
1 | ```yaml
2 | name: "Checking for Expected Latest Version and Custom Version Pattern"
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
6 |
7 | jobs:
8 | # Enforces the update of a changelog file on every pull request
9 | changelog:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2.3.4
13 |
14 | - id: read-version
15 | run: |
16 | echo "::set-output name=version::$(jq -r ".version" package.json)"
17 |
18 | - id: changelog-enforcer
19 | uses: ./
20 | with:
21 | skipLabels: "skip-changelog"
22 | # Anything within brackets that starts with `*`
23 | versionPattern: " ^\\* \\[(.*)\\]"
24 | expectedLatestVersion: ${{ steps.read-version.outputs.tag }}
25 | ```
--------------------------------------------------------------------------------
/example-workflows/with-expected-latest-version.yaml:
--------------------------------------------------------------------------------
1 | ```yaml
2 | name: "Checking for Expected Latest Version"
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
6 |
7 | jobs:
8 | # Enforces the update of a changelog file on every pull request
9 | changelog:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2.3.4
13 |
14 | - id: read-version
15 | run: |
16 | echo "::set-output name=version::$(jq -r ".version" package.json)"
17 |
18 | - id: changelog-enforcer
19 | uses: ./
20 | with:
21 | skipLabels: "skip-changelog"
22 | expectedLatestVersion: ${{ steps.read-version.outputs.tag }}
23 | ```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const changelogEnforcer = require('./src/changelog-enforcer')
2 |
3 | // Looks for a label with the name from
4 | async function run() {
5 | changelogEnforcer.enforce();
6 | }
7 |
8 | run()
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFiles: ['./__tests__/env.js'],
3 | testMatch: ['**/*.test.js'],
4 | coverageReporters: ["json-summary"]
5 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "changelog-enforcer",
3 | "version": "3.6.1",
4 | "description": "Enforces that a changelog is kept up-to-date",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "eslint index.js",
8 | "package": "ncc build index.js -o dist",
9 | "test": "eslint index.js && jest --coverage",
10 | "test:badges": "npm test && make-coverage-badge",
11 | "all": "npm run test:badges && npm run package"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/dangoslen/changelog-enforcer.git"
16 | },
17 | "keywords": [
18 | "GitHub",
19 | "Actions",
20 | "JavaScript",
21 | "Changelog"
22 | ],
23 | "author": "@dangoslen",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/dangoslen/changelog-enforcer/issues"
27 | },
28 | "homepage": "https://github.com/dangoslen/changelog-enforcer#readme",
29 | "dependencies": {
30 | "@actions/core": "^1.10.1",
31 | "@actions/github": "^6.0.0",
32 | "make-coverage-badge": "^1.2.0",
33 | "node-fetch": "^2.7.0"
34 | },
35 | "devDependencies": {
36 | "@eslint/eslintrc": "^3.1.0",
37 | "@eslint/js": "^9.7.0",
38 | "@vercel/ncc": "^0.38.1",
39 | "eslint": "^9.7.0",
40 | "globals": "^15.8.0",
41 | "jest": "^29.7.0",
42 | "y18n": "^5.0.8"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/changelog-enforcer.js:
--------------------------------------------------------------------------------
1 | const core = require('@actions/core')
2 | const github = require('@actions/github')
3 | const versionExtractor = require('./version-extractor')
4 | const labelExtractor = require('./label-extractor')
5 | const contextExtractor = require('./context-extractor')
6 | const { findChangelog, downloadChangelog } = require('./client')
7 |
8 | // Input keys
9 | const IN_CHANGELOG_PATH = 'changeLogPath'
10 | const IN_EXPECTED_LATEST_VERSION = 'expectedLatestVersion'
11 | const IN_VERSION_PATTERN = 'versionPattern'
12 | const IN_UPDATE_CUSTOM_ERROR = 'missingUpdateErrorMessage'
13 | const IN_SKIP_LABELS = 'skipLabels'
14 | const IN_TOKEN = "token"
15 |
16 | // Output keys
17 | const OUT_ERROR_MESSAGE = 'errorMessage'
18 |
19 | module.exports.enforce = async function () {
20 | try {
21 | const skipLabelList = getSkipLabels()
22 | const changeLogPath = core.getInput(IN_CHANGELOG_PATH)
23 | const missingUpdateErrorMessage = getMissingUpdateErrorMessage(changeLogPath)
24 | const expectedLatestVersion = core.getInput(IN_EXPECTED_LATEST_VERSION)
25 | const versionPattern = core.getInput(IN_VERSION_PATTERN)
26 | const token = getToken()
27 |
28 | core.info(`Skip Labels: ${skipLabelList}`)
29 | core.info(`Changelog Path: ${changeLogPath}`)
30 | core.info(`Missing Update Error Message: ${missingUpdateErrorMessage}`)
31 | core.info(`Expected Latest Version: ${expectedLatestVersion}`)
32 | core.info(`Version Pattern: ${versionPattern}`)
33 |
34 | const context = github.context
35 | const pullRequest = contextExtractor.getPullRequestContext(context)
36 | if (!pullRequest) {
37 | return
38 | }
39 |
40 | const repository = `${context.repo.owner}/${context.repo.repo}`
41 | const labelNames = pullRequest.labels.map(l => l.name)
42 | if (!shouldEnforceChangelog(labelNames, skipLabelList)) {
43 | return
44 | }
45 | const changelog = await checkChangeLog(token, repository, pullRequest.number, changeLogPath, missingUpdateErrorMessage)
46 | if (shouldEnforceVersion(expectedLatestVersion)) {
47 | return
48 | }
49 | await validateLatestVersion(token, expectedLatestVersion, versionPattern, changelog.contents_url)
50 | } catch (err) {
51 | core.setOutput(OUT_ERROR_MESSAGE, err.message)
52 | core.setFailed(err.message)
53 | }
54 | };
55 |
56 | function getSkipLabels() {
57 | const skipLabels = core.getInput(IN_SKIP_LABELS)
58 | return labelExtractor.extractLabels(skipLabels)
59 | }
60 |
61 | function getMissingUpdateErrorMessage(changeLogPath) {
62 | const customMessage = core.getInput(IN_UPDATE_CUSTOM_ERROR)
63 | if (customMessage != null && customMessage != '') {
64 | return customMessage
65 | }
66 | return `No update to ${changeLogPath} found!`
67 | }
68 |
69 | function getToken() {
70 | const token = core.getInput(IN_TOKEN)
71 | if (!token) {
72 | throw new Error("Did not find token for using the GitHub API")
73 | }
74 | return token
75 | }
76 |
77 | function shouldEnforceChangelog(labelNames, skipLabelList) {
78 | return !labelNames.some(l => skipLabelList.includes(l))
79 | }
80 |
81 | function shouldEnforceVersion(expectedLatestVersion) {
82 | return expectedLatestVersion === ''
83 | }
84 |
85 | function normalizeChangelogPath(changeLogPath) {
86 | if (changeLogPath.startsWith('./')) {
87 | return changeLogPath.substring(2)
88 | }
89 | return changeLogPath
90 | }
91 |
92 | async function checkChangeLog(token, repository, pullRequestNumber, changeLogPath, missingUpdateErrorMessage) {
93 | const normalizedChangeLogPath = normalizeChangelogPath(changeLogPath)
94 | const changelog = await findChangelog(token, repository, pullRequestNumber, 100, normalizedChangeLogPath)
95 | if (!changelog) {
96 | throw new Error(missingUpdateErrorMessage)
97 | }
98 | return changelog
99 | }
100 |
101 | async function validateLatestVersion(token, expectedLatestVersion, versionPattern, changelogUrl) {
102 | const changelog = await downloadChangelog(token, changelogUrl)
103 | const versions = versionExtractor.getVersions(versionPattern, changelog)
104 | let latest = versions[0]
105 | core.debug(`Latest version is ${latest}`)
106 | if (latest.toUpperCase() == "UNRELEASED") {
107 | if (versions.length == 1) {
108 | core.debug('There is only on unreleased version found in the changelog. Not validating expected version.')
109 | return
110 | }
111 | latest = versions[1]
112 | }
113 | if (latest !== expectedLatestVersion) {
114 | throw new Error(`The latest version in the changelog does not match the expected latest version of ${expectedLatestVersion}!`)
115 | }
116 | }
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch')
2 | const core = require('@actions/core')
3 |
4 | module.exports.findChangelog = async function (token, repository, pullRequestNumber, pageSize, changeLogPath) {
5 | let complete = false;
6 | let page = 1
7 | while (!complete) {
8 | core.debug(`Downloading page ${page} of pull request files from /repos/${repository}/pulls/${pullRequestNumber}/files`)
9 | const options = addAuth(token, {})
10 | const response = await fetch(`https://api.github.com/repos/${repository}/pulls/${pullRequestNumber}/files?per_page=${pageSize}&page=${page}`, options)
11 | if (!response.ok) {
12 | throw new Error(`Got a ${response.status} response from GitHub API`)
13 | }
14 | const files = await response.json()
15 | core.debug(`Downloaded page ${page} of pull request files`)
16 |
17 | core.debug("Filtering for changelog")
18 | const filtered = files
19 | .filter(f => f.status !== 'deleted')
20 | .filter(f => f.filename === changeLogPath)
21 |
22 | if (filtered.length == 1) {
23 | return filtered[0]
24 | } else if (files.length < pageSize) {
25 | complete = true
26 | } else {
27 | page++
28 | }
29 | }
30 | return undefined
31 | }
32 |
33 | module.exports.downloadChangelog = async function (token, changelogUrl) {
34 | core.debug(`Downloading changelog from ${changelogUrl}`)
35 | const apiVersion = 'v3'
36 | const mediaTypeHeader = {
37 | 'headers': {
38 | 'Accept': `application/vnd.github.${apiVersion}.raw`
39 | }
40 | }
41 | const options = addAuth(token, mediaTypeHeader)
42 | const response = await fetch(`${changelogUrl}`, options)
43 | if (!response.ok) {
44 | throw new Error(`Got a ${response.status} response from GitHub API`)
45 | }
46 | const changelog = await response.text()
47 | core.debug("Downloaded changelog")
48 | return changelog
49 | }
50 |
51 | function addAuth(token, options) {
52 | const enriched = { ...options }
53 | if (!enriched['headers']) {
54 | enriched['headers'] = {}
55 | }
56 | enriched['headers']['Authorization'] = `Bearer ${token}`
57 | return enriched
58 | }
--------------------------------------------------------------------------------
/src/context-extractor.js:
--------------------------------------------------------------------------------
1 | const core = require('@actions/core')
2 |
3 | module.exports.getPullRequestContext = function (context) {
4 | const pull_request = context.payload.pull_request
5 | if (pull_request == undefined) {
6 | core.warning(`ChangeLog enforcer only runs for pull_request and pull_request_target event types`)
7 | return undefined
8 | }
9 | return context.payload.pull_request;
10 | }
--------------------------------------------------------------------------------
/src/label-extractor.js:
--------------------------------------------------------------------------------
1 | module.exports.extractLabels = function (labelsString) {
2 | // Parses a list of labels. Each label can be of any length and will either end with a comma or be the end of the string.
3 | // Matches
4 | // - words ("\w")
5 | // - whitespace characters ("\s")
6 | // - dashes ("-")
7 | // -plus signs ("+")
8 | // - questions marks ("?")
9 | // - semi-colons (";")
10 | // - colons (":") - this is for emoji usage
11 | // - brackets ("[" and "\]")
12 | // - parenthesis ("(" and ")")
13 | // - forward-slashes ("/")
14 | // Each match may are may not have a trailing comma (,?). If one exists, it is removed before appending it to the list
15 | const regex = new RegExp(/([\w\s-/+?;:[\]()]+,?)/, 'g')
16 | let labels = []
17 | let groups
18 | do {
19 | groups = regex.exec(labelsString)
20 | if (groups) {
21 | // Removes the trailing comma and removes all whitespace
22 | let label = groups[0].replace(",", "").trim()
23 | labels.push(label)
24 | }
25 | } while(groups)
26 | return labels
27 | }
28 |
--------------------------------------------------------------------------------
/src/version-extractor.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const core = require('@actions/core')
3 |
4 | module.exports.getVersions = function (pattern, changelog) {
5 | const regex = new RegExp(`${pattern}`, 'gm')
6 | let groups = false
7 | let versions = []
8 | do {
9 | groups = regex.exec(changelog)
10 | if (groups) {
11 | // The actual group we want to match is the version
12 | core.debug(`Found version ${groups[1]}`)
13 | versions.push(groups[1])
14 | }
15 | } while (groups)
16 | return versions
17 | }
--------------------------------------------------------------------------------