├── .gitignore ├── dist └── templates │ ├── commit.hbs │ ├── footer.hbs │ ├── header.hbs │ └── template.hbs ├── index.js ├── .github ├── FUNDING.yml ├── workflows │ ├── tagger.yml │ ├── lint-pr-title.yml │ ├── lint-pr-title-preview.yml │ ├── release.yml │ ├── lint-pr-title-preview-validateSingleCommit.yml │ ├── lint-pr-title-preview-ignoreLabels.yml │ ├── test.yml │ ├── lint-pr-title-preview-outputErrorMessage.yml │ └── lint-pr-title-preview-all.yml ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── eslint.config.mjs ├── src ├── formatMessage.js ├── ConfigParser.js ├── ConfigParser.test.js ├── formatMessage.test.js ├── parseConfig.js ├── validatePrTitle.js ├── index.js └── validatePrTitle.test.js ├── rollup.config.js ├── .releaserc.json ├── LICENSE ├── CONTRIBUTORS.md ├── package.json ├── action.yml ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /dist/templates/commit.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/templates/footer.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/templates/header.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/templates/template.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const run = require('./src'); 2 | 3 | run(); 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: amannn 4 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {getPresets} from 'eslint-config-molindo'; 2 | import globals from 'globals'; 3 | 4 | export default [ 5 | ...(await getPresets('javascript', 'vitest')), 6 | { 7 | languageOptions: { 8 | globals: globals.node 9 | } 10 | } 11 | ]; 12 | -------------------------------------------------------------------------------- /src/formatMessage.js: -------------------------------------------------------------------------------- 1 | export default function formatMessage(message, values) { 2 | let formatted = message; 3 | if (values) { 4 | Object.entries(values).forEach(([key, value]) => { 5 | formatted = formatted.replace(new RegExp(`{${key}}`, 'g'), value); 6 | }); 7 | } 8 | return formatted; 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/tagger.yml: -------------------------------------------------------------------------------- 1 | name: 'Tag release for latest major version' 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | actions-tagger: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - uses: Actions-R-Us/actions-tagger@v2 14 | -------------------------------------------------------------------------------- /src/ConfigParser.js: -------------------------------------------------------------------------------- 1 | const ENUM_SPLIT_REGEX = /\n/; 2 | 3 | export default { 4 | parseEnum(input) { 5 | return input 6 | .split(ENUM_SPLIT_REGEX) 7 | .map((part) => part.trim()) 8 | .filter((part) => part.length > 0); 9 | }, 10 | 11 | parseBoolean(input) { 12 | return JSON.parse(input.trim()); 13 | }, 14 | 15 | parseString(input) { 16 | return input; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'quarterly' 7 | groups: 8 | npm-packages: 9 | patterns: ['*'] 10 | 11 | - package-ecosystem: 'github-actions' 12 | directory: '/' 13 | schedule: 14 | interval: 'quarterly' 15 | groups: 16 | github-action-workflows: 17 | patterns: ['*'] 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To reproduce** 14 | 15 | Please include a reproduction repository and outline the steps that are needed to run into the issue. 16 | 17 | **Expected behavior** 18 | 19 | A clear and concise description of what you expected to happen. 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // See: https://rollupjs.org/introduction/ 2 | 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 6 | 7 | const config = { 8 | input: 'src/index.js', 9 | output: { 10 | esModule: true, 11 | file: 'dist/index.js', 12 | format: 'es', 13 | sourcemap: true 14 | }, 15 | plugins: [commonjs(), nodeResolve({preferBuiltins: true}), json()] 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title' 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: read 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - uses: ./ 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 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 | 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | -------------------------------------------------------------------------------- /src/ConfigParser.test.js: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest'; 2 | import ConfigParser from './ConfigParser.js'; 3 | 4 | describe('parseEnum', () => { 5 | it('parses newline-delimited lists, trimming whitespace', () => { 6 | expect(ConfigParser.parseEnum('one \ntwo \nthree \r\nfour')).toEqual([ 7 | 'one', 8 | 'two', 9 | 'three', 10 | 'four' 11 | ]); 12 | }); 13 | it('parses newline-delimited lists, including regex, trimming whitespace', () => { 14 | expect( 15 | ConfigParser.parseEnum('one \ntwo \n^[A-Z]+\\n$ \r\nfour') 16 | ).toEqual(['one', 'two', '^[A-Z]+\\n$', 'four']); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title-preview.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title preview (current branch)' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: read 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - run: pnpm install 23 | - run: pnpm build 24 | - uses: ./ 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | deployments: write 13 | issues: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | version: 9 20 | - uses: actions/setup-node@v5 21 | with: 22 | node-version: 24 23 | - run: pnpm install 24 | - run: pnpm build 25 | - run: pnpm semantic-release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "conventionalcommits", 3 | "branches": ["main"], 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | ["@semantic-release/changelog", {"changelogTitle": "# Changelog"}], 8 | "semantic-release-major-tag", 9 | [ 10 | "@semantic-release/github", 11 | { 12 | "failComment": false, 13 | "failTitle": false, 14 | "releasedLabels": false 15 | } 16 | ], 17 | [ 18 | "@semantic-release/git", 19 | { 20 | "assets": ["dist", "CHANGELOG.md"], 21 | "message": "chore: Release ${nextRelease.version} [skip ci]" 22 | } 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title-preview-validateSingleCommit.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title preview (current branch, validateSingleCommit enabled)' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: read 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - run: pnpm install 23 | - run: pnpm build 24 | - uses: ./ 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | validateSingleCommit: true 29 | validateSingleCommitMatchesPrTitle: true 30 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title-preview-ignoreLabels.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title preview (current branch, ignoreLabels enabled)' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | - labeled 9 | - unlabeled 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: read 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9 21 | - uses: actions/setup-node@v5 22 | with: 23 | node-version: 24 24 | - run: pnpm install 25 | - run: pnpm build 26 | - uses: ./ 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | with: 30 | ignoreLabels: | 31 | bot 32 | ignore-semantic-pull-request 33 | -------------------------------------------------------------------------------- /src/formatMessage.test.js: -------------------------------------------------------------------------------- 1 | import {expect, it} from 'vitest'; 2 | import formatMessage from './formatMessage.js'; 3 | 4 | it('handles a string without variables', () => { 5 | const message = 'this is test'; 6 | expect(formatMessage(message)).toEqual(message); 7 | }); 8 | 9 | it('replaces a variable', () => { 10 | expect( 11 | formatMessage('this is {subject} test', {subject: 'my subject'}) 12 | ).toEqual('this is my subject test'); 13 | }); 14 | 15 | it('replaces multiple variables', () => { 16 | expect( 17 | formatMessage('this {title} is {subject} test', { 18 | subject: 'my subject', 19 | title: 'my title' 20 | }) 21 | ).toEqual('this my title is my subject test'); 22 | }); 23 | 24 | it('replaces multiple instances of a variable', () => { 25 | expect( 26 | formatMessage( 27 | '99 bottles of {beverage} on the wall, 99 bottles of {beverage}.', 28 | {beverage: 'beer'} 29 | ) 30 | ).toEqual('99 bottles of beer on the wall, 99 bottles of beer.'); 31 | }); 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - run: pnpm install 23 | - run: pnpm lint && pnpm test 24 | 25 | dist: 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | steps: 30 | - uses: actions/checkout@v5 31 | with: 32 | fetch-depth: 0 33 | - name: Check if `dist/` has been modified. 34 | run: | 35 | set -euxo pipefail 36 | 37 | if [ $(git diff origin/main --name-only -- 'dist/**' | wc -l) -gt 0 ] 38 | then 39 | echo "Please do not update 'dist/'. CI will update it automatically on merge." 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Contributors 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 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Thank you very much for contributing to this project! 4 | 5 | Due to the way event triggers work with GitHub actions it's a bit harder to test your changes. 6 | 7 | Simple changes that can be unit tested can be implemented with the regular workflow where you fork the repo and create a pull request. Note however that the new version of the action won't be executed in the workflows of this repository. We have to rely on the unit tests in this case. 8 | 9 | If e.g. environment parameters are changed, the action should be tested end-to-end in a workflow. 10 | 11 | To do this, please follow this process: 12 | 13 | 1. Fork the repo. 14 | 2. Create a PR in **your own repo**. 15 | 3. The "Lint PR title preview (current branch)" workflow will run the new version and will help you validate the change. 16 | 4. Create a PR to this repo with the changes. In this case the preview workflow will fail, but we'll know that it works since you've tested it in the fork. Please include a include a link to a workflow where you've tested the current state of this pull request. 17 | 5. Don't run `npm run build` to update the `dist` folder as it will be generated on CI during the build 18 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title-preview-outputErrorMessage.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title preview (current branch, outputErrorMessage)' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: write 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - run: pnpm install 23 | - run: pnpm build 24 | - uses: ./ 25 | id: lint_pr_title 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | - uses: marocchino/sticky-pull-request-comment@v2 29 | # When the previous steps fails, the workflow would stop. By adding this 30 | # condition you can continue the execution with the populated error message. 31 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 32 | with: 33 | header: pr-title-lint-error 34 | message: | 35 | Hey there and thank you for opening this pull request! 👋🏼 36 | 37 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 38 | 39 | Details: 40 | 41 | ``` 42 | ${{ steps.lint_pr_title.outputs.error_message }} 43 | ``` 44 | # Delete a previous comment when the issue has been resolved 45 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 46 | uses: marocchino/sticky-pull-request-comment@v2 47 | with: 48 | header: pr-title-lint-error 49 | delete: true 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-semantic-pull-request", 3 | "version": "0.0.0", 4 | "description": "Ensure your PR title matches the Conventional Commits spec.", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "eslint src && prettier --check src", 9 | "test": "vitest src", 10 | "build": "rollup -c" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/amannn/action-semantic-pull-request.git" 15 | }, 16 | "keywords": [ 17 | "github-action", 18 | "conventional-commits" 19 | ], 20 | "author": "Jan Amann ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/amannn/action-semantic-pull-request/issues" 24 | }, 25 | "homepage": "https://github.com/amannn/action-semantic-pull-request#readme", 26 | "dependencies": { 27 | "@actions/core": "^1.11.1", 28 | "@actions/github": "^6.0.1", 29 | "conventional-changelog-conventionalcommits": "9.1.0", 30 | "conventional-commit-types": "^3.0.0", 31 | "conventional-commits-parser": "^6.2.0" 32 | }, 33 | "devDependencies": { 34 | "@rollup/plugin-commonjs": "^28.0.6", 35 | "@rollup/plugin-json": "^6.1.0", 36 | "@rollup/plugin-node-resolve": "^16.0.1", 37 | "@semantic-release/changelog": "6.0.3", 38 | "@semantic-release/commit-analyzer": "13.0.1", 39 | "@semantic-release/git": "10.0.1", 40 | "@semantic-release/github": "11.0.6", 41 | "@semantic-release/release-notes-generator": "14.1.0", 42 | "eslint": "9.36.0", 43 | "eslint-config-molindo": "8.0.0", 44 | "globals": "^16.4.0", 45 | "prettier": "^3.6.2", 46 | "rollup": "^4.52.3", 47 | "semantic-release": "^24.2.9", 48 | "semantic-release-major-tag": "0.3.2", 49 | "vitest": "^3.2.4" 50 | }, 51 | "engines": { 52 | "node": "^24.0.0" 53 | }, 54 | "prettier": "eslint-config-molindo/.prettierrc.json" 55 | } 56 | -------------------------------------------------------------------------------- /src/parseConfig.js: -------------------------------------------------------------------------------- 1 | import ConfigParser from './ConfigParser.js'; 2 | 3 | export default function parseConfig() { 4 | let types; 5 | if (process.env.INPUT_TYPES) { 6 | types = ConfigParser.parseEnum(process.env.INPUT_TYPES); 7 | } 8 | 9 | let scopes; 10 | if (process.env.INPUT_SCOPES) { 11 | scopes = ConfigParser.parseEnum(process.env.INPUT_SCOPES); 12 | } 13 | 14 | let requireScope; 15 | if (process.env.INPUT_REQUIRESCOPE) { 16 | requireScope = ConfigParser.parseBoolean(process.env.INPUT_REQUIRESCOPE); 17 | } 18 | 19 | let disallowScopes; 20 | if (process.env.INPUT_DISALLOWSCOPES) { 21 | disallowScopes = ConfigParser.parseEnum(process.env.INPUT_DISALLOWSCOPES); 22 | } 23 | 24 | let subjectPattern; 25 | if (process.env.INPUT_SUBJECTPATTERN) { 26 | subjectPattern = ConfigParser.parseString(process.env.INPUT_SUBJECTPATTERN); 27 | } 28 | 29 | let subjectPatternError; 30 | if (process.env.INPUT_SUBJECTPATTERNERROR) { 31 | subjectPatternError = ConfigParser.parseString( 32 | process.env.INPUT_SUBJECTPATTERNERROR 33 | ); 34 | } 35 | 36 | let headerPattern; 37 | if (process.env.INPUT_HEADERPATTERN) { 38 | headerPattern = ConfigParser.parseString(process.env.INPUT_HEADERPATTERN); 39 | } 40 | 41 | let headerPatternCorrespondence; 42 | if (process.env.INPUT_HEADERPATTERNCORRESPONDENCE) { 43 | // todo: this should be migrated to an enum w/ ConfigParser.parseEnum 44 | headerPatternCorrespondence = 45 | process.env.INPUT_HEADERPATTERNCORRESPONDENCE.split(',') 46 | .map((part) => part.trim()) 47 | .filter((part) => part.length > 0); 48 | } 49 | 50 | let wip; 51 | if (process.env.INPUT_WIP) { 52 | wip = ConfigParser.parseBoolean(process.env.INPUT_WIP); 53 | } 54 | 55 | let validateSingleCommit; 56 | if (process.env.INPUT_VALIDATESINGLECOMMIT) { 57 | validateSingleCommit = ConfigParser.parseBoolean( 58 | process.env.INPUT_VALIDATESINGLECOMMIT 59 | ); 60 | } 61 | 62 | let validateSingleCommitMatchesPrTitle; 63 | if (process.env.INPUT_VALIDATESINGLECOMMITMATCHESPRTITLE) { 64 | validateSingleCommitMatchesPrTitle = ConfigParser.parseBoolean( 65 | process.env.INPUT_VALIDATESINGLECOMMITMATCHESPRTITLE 66 | ); 67 | } 68 | 69 | let githubBaseUrl; 70 | if (process.env.INPUT_GITHUBBASEURL) { 71 | githubBaseUrl = ConfigParser.parseString(process.env.INPUT_GITHUBBASEURL); 72 | } 73 | 74 | let ignoreLabels; 75 | if (process.env.INPUT_IGNORELABELS) { 76 | ignoreLabels = ConfigParser.parseEnum(process.env.INPUT_IGNORELABELS); 77 | } 78 | 79 | return { 80 | types, 81 | scopes, 82 | requireScope, 83 | disallowScopes, 84 | wip, 85 | subjectPattern, 86 | subjectPatternError, 87 | headerPattern, 88 | headerPatternCorrespondence, 89 | validateSingleCommit, 90 | validateSingleCommitMatchesPrTitle, 91 | githubBaseUrl, 92 | ignoreLabels 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: semantic-pull-request 2 | author: Jan Amann 3 | description: Ensure your PR title matches the Conventional Commits spec (https://www.conventionalcommits.org/). 4 | runs: 5 | using: 'node24' 6 | main: 'dist/index.js' 7 | branding: 8 | icon: 'shield' 9 | color: 'green' 10 | inputs: 11 | types: 12 | description: "Provide custom types (newline delimited) if you don't want the default ones from https://www.conventionalcommits.org. These are regex patterns auto-wrapped in `^ $`." 13 | required: false 14 | scopes: 15 | description: 'Configure which scopes are allowed (newline delimited). These are regex patterns auto-wrapped in `^ $`.' 16 | required: false 17 | requireScope: 18 | description: 'Configure that a scope must always be provided.' 19 | required: false 20 | disallowScopes: 21 | description: 'Configure which scopes are disallowed in PR titles (newline delimited). These are regex patterns auto-wrapped in ` ^$`.' 22 | required: false 23 | subjectPattern: 24 | description: "Configure additional validation for the subject based on a regex. E.g. '^(?![A-Z]).+$' ensures the subject doesn't start with an uppercase character." 25 | required: false 26 | subjectPatternError: 27 | description: "If `subjectPattern` is configured, you can use this property to override the default error message that is shown when the pattern doesn't match. The variables `subject` and `title` can be used within the message." 28 | required: false 29 | validateSingleCommit: 30 | description: 'When using "Squash and merge" on a PR with only one commit, GitHub will suggest using that commit message instead of the PR title for the merge commit, and it''s easy to commit this by mistake. Enable this option to also validate the commit message for one commit PRs.' 31 | required: false 32 | validateSingleCommitMatchesPrTitle: 33 | description: 'Related to `validateSingleCommit` you can opt-in to validate that the PR title matches a single commit to avoid confusion.' 34 | required: false 35 | githubBaseUrl: 36 | description: 'The GitHub base URL will be automatically set to the correct value from the GitHub context variable. If you want to override this, you can do so here (not recommended).' 37 | required: false 38 | default: '${{ github.api_url }}' 39 | ignoreLabels: 40 | description: 'If the PR contains one of these labels (newline delimited), the validation is skipped. If you want to rerun the validation when labels change, you might want to use the `labeled` and `unlabeled` event triggers in your workflow.' 41 | required: false 42 | headerPattern: 43 | description: "If you're using a format for the PR title that differs from the traditional Conventional Commits spec, you can use this to customize the parsing of the type, scope and subject. The `headerPattern` should contain a regex where the capturing groups in parentheses correspond to the parts listed in `headerPatternCorrespondence`. For more details see: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern" 44 | required: false 45 | headerPatternCorrespondence: 46 | description: 'If `headerPattern` is configured, you can use this to define which capturing groups correspond to the type, scope and subject.' 47 | required: false 48 | wip: 49 | description: "For work-in-progress PRs you can typically use draft pull requests from Github. However, private repositories on the free plan don't have this option and therefore this action allows you to opt-in to using the special '[WIP]' prefix to indicate this state. This will avoid the validation of the PR title and the pull request checks remain pending. Note that a second check will be reported if this is enabled." 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title-preview-all.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR title preview (current branch, all options specified)' 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: read 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v5 20 | with: 21 | node-version: 24 22 | - run: pnpm install 23 | - run: pnpm build 24 | - uses: ./ 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | # Configure which types are allowed (newline-delimited). 29 | # Default: https://github.com/commitizen/conventional-commit-types 30 | types: | 31 | feat 32 | fix 33 | docs 34 | style 35 | refactor 36 | perf 37 | test 38 | build 39 | ci 40 | chore 41 | revert 42 | # Configure which scopes are allowed (newline-delimited). 43 | # These are regex patterns auto-wrapped in `^ $`. 44 | scopes: | 45 | core 46 | ui 47 | JIRA-\d+ 48 | # Configure that a scope must always be provided. 49 | requireScope: false 50 | # Configure which scopes are disallowed in PR titles (newline-delimited). 51 | # For instance by setting the value below, `chore(release): ...` (lowercase) 52 | # and `ci(e2e,release): ...` (unknown scope) will be rejected. 53 | # These are regex patterns auto-wrapped in `^ $`. 54 | disallowScopes: | 55 | release 56 | [A-Z]+ 57 | # Configure additional validation for the subject based on a regex. 58 | # This example ensures the subject doesn't start with an uppercase character. 59 | subjectPattern: ^(?![A-Z]).+$ 60 | # If `subjectPattern` is configured, you can use this property to override 61 | # the default error message that is shown when the pattern doesn't match. 62 | # The variables `subject` and `title` can be used within the message. 63 | subjectPatternError: | 64 | The subject "{subject}" found in the pull request title "{title}" 65 | didn't match the configured pattern. Please ensure that the subject 66 | doesn't start with an uppercase character. 67 | # The GitHub base URL will be automatically set to the correct value from the GitHub context variable. 68 | # If you want to override this, you can do so here (not recommended). 69 | # githubBaseUrl: https://github.com/api/v3 70 | # If the PR contains one of these newline-delimited labels, the 71 | # validation is skipped. If you want to rerun the validation when 72 | # labels change, you might want to use the `labeled` and `unlabeled` 73 | # event triggers in your workflow. 74 | ignoreLabels: | 75 | bot 76 | ignore-semantic-pull-request 77 | # If you're using a format for the PR title that differs from the traditional Conventional 78 | # Commits spec, you can use these options to customize the parsing of the type, scope and 79 | # subject. The `headerPattern` should contain a regex where the capturing groups in parentheses 80 | # correspond to the parts listed in `headerPatternCorrespondence`. 81 | # See: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern 82 | headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$' 83 | headerPatternCorrespondence: type, scope, subject 84 | -------------------------------------------------------------------------------- /src/validatePrTitle.js: -------------------------------------------------------------------------------- 1 | import core from '@actions/core'; 2 | // eslint-disable-next-line import/no-unresolved -- False positive 3 | import conventionalCommitsConfig from 'conventional-changelog-conventionalcommits'; 4 | import conventionalCommitTypes from 'conventional-commit-types'; 5 | // eslint-disable-next-line import/no-unresolved -- False positive 6 | import {CommitParser} from 'conventional-commits-parser'; 7 | import formatMessage from './formatMessage.js'; 8 | 9 | const defaultTypes = Object.keys(conventionalCommitTypes.types); 10 | 11 | export default async function validatePrTitle( 12 | prTitle, 13 | { 14 | types, 15 | scopes, 16 | requireScope, 17 | disallowScopes, 18 | subjectPattern, 19 | subjectPatternError, 20 | headerPattern, 21 | headerPatternCorrespondence 22 | } = {} 23 | ) { 24 | if (!types) types = defaultTypes; 25 | 26 | const {parser: parserOpts} = await conventionalCommitsConfig(); 27 | if (headerPattern) { 28 | parserOpts.headerPattern = headerPattern; 29 | } 30 | if (headerPatternCorrespondence) { 31 | parserOpts.headerCorrespondence = headerPatternCorrespondence; 32 | } 33 | const result = new CommitParser(parserOpts).parse(prTitle); 34 | 35 | core.setOutput('type', result.type); 36 | core.setOutput('scope', result.scope); 37 | core.setOutput('subject', result.subject); 38 | 39 | function printAvailableTypes() { 40 | return `Available types:\n${types 41 | .map((type) => { 42 | let bullet = ` - ${type}`; 43 | 44 | if (types === defaultTypes) { 45 | bullet += `: ${conventionalCommitTypes.types[type].description}`; 46 | } 47 | 48 | return bullet; 49 | }) 50 | .join('\n')}`; 51 | } 52 | 53 | function isUnknownScope(s) { 54 | return scopes && !scopes.some((scope) => new RegExp(`^${scope}$`).test(s)); 55 | } 56 | 57 | function isDisallowedScope(s) { 58 | return ( 59 | disallowScopes && 60 | disallowScopes.some((scope) => new RegExp(`^${scope}$`).test(s)) 61 | ); 62 | } 63 | 64 | if (!result.type) { 65 | raiseError( 66 | `No release type found in pull request title "${prTitle}". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/\n\n${printAvailableTypes()}` 67 | ); 68 | } 69 | 70 | if (!result.subject) { 71 | raiseError(`No subject found in pull request title "${prTitle}".`); 72 | } 73 | 74 | if (!types.some((type) => new RegExp(`^${type}$`).test(result.type))) { 75 | raiseError( 76 | `Unknown release type "${ 77 | result.type 78 | }" found in pull request title "${prTitle}".\n\n${printAvailableTypes()}` 79 | ); 80 | } 81 | 82 | if (requireScope && !result.scope) { 83 | let message = `No scope found in pull request title "${prTitle}".`; 84 | if (scopes) { 85 | message += ` Scope must match one of: ${scopes.join(', ')}.`; 86 | } 87 | raiseError(message); 88 | } 89 | 90 | const givenScopes = result.scope 91 | ? result.scope.split(',').map((scope) => scope.trim()) 92 | : undefined; 93 | 94 | const unknownScopes = givenScopes ? givenScopes.filter(isUnknownScope) : []; 95 | if (scopes && unknownScopes.length > 0) { 96 | raiseError( 97 | `Unknown ${ 98 | unknownScopes.length > 1 ? 'scopes' : 'scope' 99 | } "${unknownScopes.join( 100 | ',' 101 | )}" found in pull request title "${prTitle}". Scope must match one of: ${scopes.join( 102 | ', ' 103 | )}.` 104 | ); 105 | } 106 | 107 | const disallowedScopes = givenScopes 108 | ? givenScopes.filter(isDisallowedScope) 109 | : []; 110 | if (disallowScopes && disallowedScopes.length > 0) { 111 | raiseError( 112 | `Disallowed ${ 113 | disallowedScopes.length === 1 ? 'scope was' : 'scopes were' 114 | } found: ${disallowedScopes.join(', ')}` 115 | ); 116 | } 117 | 118 | function throwSubjectPatternError(message) { 119 | if (subjectPatternError) { 120 | message = formatMessage(subjectPatternError, { 121 | subject: result.subject, 122 | title: prTitle 123 | }); 124 | } 125 | raiseError(message); 126 | } 127 | 128 | if (subjectPattern) { 129 | const match = result.subject.match(new RegExp(subjectPattern)); 130 | 131 | if (!match) { 132 | throwSubjectPatternError( 133 | `The subject "${result.subject}" found in pull request title "${prTitle}" doesn't match the configured pattern "${subjectPattern}".` 134 | ); 135 | } 136 | 137 | const matchedPart = match[0]; 138 | if (matchedPart.length !== result.subject.length) { 139 | throwSubjectPatternError( 140 | `The subject "${result.subject}" found in pull request title "${prTitle}" isn't an exact match for the configured pattern "${subjectPattern}". Please provide a subject that matches the whole pattern exactly.` 141 | ); 142 | } 143 | } 144 | 145 | function raiseError(message) { 146 | core.setOutput('error_message', message); 147 | 148 | throw new Error(message); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import core from '@actions/core'; 2 | import github from '@actions/github'; 3 | import parseConfig from './parseConfig.js'; 4 | import validatePrTitle from './validatePrTitle.js'; 5 | 6 | async function run() { 7 | try { 8 | const { 9 | types, 10 | scopes, 11 | requireScope, 12 | disallowScopes, 13 | wip, 14 | subjectPattern, 15 | subjectPatternError, 16 | headerPattern, 17 | headerPatternCorrespondence, 18 | validateSingleCommit, 19 | validateSingleCommitMatchesPrTitle, 20 | githubBaseUrl, 21 | ignoreLabels 22 | } = parseConfig(); 23 | 24 | const client = github.getOctokit(process.env.GITHUB_TOKEN, { 25 | baseUrl: githubBaseUrl 26 | }); 27 | 28 | const contextPullRequest = github.context.payload.pull_request; 29 | if (!contextPullRequest) { 30 | throw new Error( 31 | "This action can only be invoked in `pull_request_target` or `pull_request` events. Otherwise the pull request can't be inferred." 32 | ); 33 | } 34 | 35 | const owner = contextPullRequest.base.user.login; 36 | const repo = contextPullRequest.base.repo.name; 37 | 38 | // The pull request info on the context isn't up to date. When 39 | // the user updates the title and re-runs the workflow, it would 40 | // be outdated. Therefore fetch the pull request via the REST API 41 | // to ensure we use the current title. 42 | const {data: pullRequest} = await client.rest.pulls.get({ 43 | owner, 44 | repo, 45 | pull_number: contextPullRequest.number 46 | }); 47 | 48 | // Ignore errors if specified labels are added. 49 | if (ignoreLabels) { 50 | const labelNames = pullRequest.labels.map((label) => label.name); 51 | for (const labelName of labelNames) { 52 | if (ignoreLabels.includes(labelName)) { 53 | core.info( 54 | `Validation was skipped because the PR label "${labelName}" was found.` 55 | ); 56 | return; 57 | } 58 | } 59 | } 60 | 61 | // Pull requests that start with "[WIP] " are excluded from the check. 62 | const isWip = wip && /^\[WIP\]\s/.test(pullRequest.title); 63 | 64 | let validationError; 65 | if (!isWip) { 66 | try { 67 | await validatePrTitle(pullRequest.title, { 68 | types, 69 | scopes, 70 | requireScope, 71 | disallowScopes, 72 | subjectPattern, 73 | subjectPatternError, 74 | headerPattern, 75 | headerPatternCorrespondence 76 | }); 77 | 78 | if (validateSingleCommit) { 79 | const commits = []; 80 | let nonMergeCommits = []; 81 | 82 | for await (const response of client.paginate.iterator( 83 | client.rest.pulls.listCommits, 84 | { 85 | owner, 86 | repo, 87 | pull_number: contextPullRequest.number 88 | } 89 | )) { 90 | commits.push(...response.data); 91 | 92 | // GitHub does not count merge commits when deciding whether to use 93 | // the PR title or a commit message for the squash commit message. 94 | nonMergeCommits = commits.filter( 95 | (commit) => commit.parents.length < 2 96 | ); 97 | 98 | // We only need two non-merge commits to know that the PR 99 | // title won't be used. 100 | if (nonMergeCommits.length >= 2) break; 101 | } 102 | 103 | // If there is only one (non merge) commit present, GitHub will use 104 | // that commit rather than the PR title for the title of a squash 105 | // commit. To make sure a semantic title is used for the squash 106 | // commit, we need to validate the commit title. 107 | if (nonMergeCommits.length === 1) { 108 | try { 109 | await validatePrTitle(nonMergeCommits[0].commit.message, { 110 | types, 111 | scopes, 112 | requireScope, 113 | disallowScopes, 114 | subjectPattern, 115 | subjectPatternError, 116 | headerPattern, 117 | headerPatternCorrespondence 118 | }); 119 | // eslint-disable-next-line unicorn/prefer-optional-catch-binding, no-unused-vars -- Legacy syntax for compatibility 120 | } catch (error) { 121 | throw new Error( 122 | `Pull request has only one commit and it's not semantic; this may lead to a non-semantic commit in the base branch (see https://github.com/community/community/discussions/16271 ). Amend the commit message to match the pull request title, or add another commit.` 123 | ); 124 | } 125 | 126 | if (validateSingleCommitMatchesPrTitle) { 127 | const commitTitle = 128 | nonMergeCommits[0].commit.message.split('\n')[0]; 129 | if (commitTitle !== pullRequest.title) { 130 | throw new Error( 131 | `The pull request has only one (non-merge) commit and in this case Github will use it as the default commit message when merging. The pull request title doesn't match the commit though ("${pullRequest.title}" vs. "${commitTitle}"). Please update the pull request title accordingly to avoid surprises.` 132 | ); 133 | } 134 | } 135 | } 136 | } 137 | } catch (error) { 138 | validationError = error; 139 | } 140 | } 141 | 142 | if (wip) { 143 | const newStatus = 144 | isWip || validationError != null ? 'pending' : 'success'; 145 | 146 | // When setting the status to "pending", the checks don't 147 | // complete. This can be used for WIP PRs in repositories 148 | // which don't support draft pull requests. 149 | // https://developer.github.com/v3/repos/statuses/#create-a-status 150 | await client.request('POST /repos/:owner/:repo/statuses/:sha', { 151 | owner, 152 | repo, 153 | sha: pullRequest.head.sha, 154 | state: newStatus, 155 | target_url: 'https://github.com/amannn/action-semantic-pull-request', 156 | description: isWip 157 | ? 'This PR is marked with "[WIP]".' 158 | : validationError 159 | ? 'PR title validation failed' 160 | : 'Ready for review & merge.', 161 | context: 'action-semantic-pull-request' 162 | }); 163 | } 164 | 165 | if (!isWip && validationError) { 166 | throw validationError; 167 | } 168 | } catch (error) { 169 | core.setFailed(error.message); 170 | } 171 | } 172 | 173 | await run(); 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # action-semantic-pull-request 2 | 3 | This is a GitHub Action that ensures that your pull request titles match the [Conventional Commits spec](https://www.conventionalcommits.org/). Typically, this is used in combination with a tool like [semantic-release](https://github.com/semantic-release/semantic-release) to automate releases. 4 | 5 | Used by: [Electron](https://github.com/electron/electron) · [Vite](https://github.com/vitejs/vite) · [Excalidraw](https://github.com/excalidraw/excalidraw) · [Apache](https://github.com/apache/pulsar) · [Vercel](https://github.com/vercel/ncc) · [Microsoft](https://github.com/microsoft/SynapseML) · [Firebase](https://github.com/firebase/flutterfire) · [AWS](https://github.com/aws-ia/terraform-aws-eks-blueprints) – and [many more](https://github.com/amannn/action-semantic-pull-request/network/dependents). 6 | 7 | ## Examples 8 | 9 | **Valid pull request titles:** 10 | 11 | - fix: Correct typo 12 | - feat: Add support for Node.js 18 13 | - refactor!: Drop support for Node.js 12 14 | - feat(ui): Add `Button` component 15 | 16 | > Note that since pull request titles only have a single line, you have to use `!` to indicate breaking changes. 17 | 18 | See [Conventional Commits](https://www.conventionalcommits.org/) for more examples. 19 | 20 | ## Installation 21 | 22 | 1. If your goal is to create squashed commits that will be used for automated releases, you'll want to configure your GitHub repository to [use the squash & merge strategy](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/configuring-commit-squashing-for-pull-requests) and tick the option "Default to PR title for squash merge commits". 23 | 2. [Add the action](https://docs.github.com/en/actions/quickstart) with the following configuration: 24 | 25 | ```yml 26 | name: 'Lint PR' 27 | 28 | on: 29 | pull_request_target: 30 | types: 31 | - opened 32 | - reopened 33 | - edited 34 | # - synchronize (if you use required Actions) 35 | 36 | jobs: 37 | main: 38 | name: Validate PR title 39 | runs-on: ubuntu-latest 40 | permissions: 41 | pull-requests: read 42 | steps: 43 | - uses: amannn/action-semantic-pull-request@v6 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | ``` 47 | 48 | See the [event triggers documentation](#event-triggers) below to learn more about what `pull_request_target` means. 49 | 50 | ## Configuration 51 | 52 | The action works without configuration, however you can provide options for customization. 53 | 54 | The following terminology helps to understand the configuration options: 55 | 56 | ``` 57 | feat(ui): Add `Button` component 58 | ^ ^ ^ 59 | | | |__ Subject 60 | | |_______ Scope 61 | |____________ Type 62 | ``` 63 | 64 | ```yml 65 | with: 66 | # Configure which types are allowed (newline-delimited). 67 | # These are regex patterns auto-wrapped in `^ $`. 68 | # Default: https://github.com/commitizen/conventional-commit-types 69 | types: | 70 | fix 71 | feat 72 | JIRA-\d+ 73 | # Configure which scopes are allowed (newline-delimited). 74 | # These are regex patterns auto-wrapped in `^ $`. 75 | scopes: | 76 | core 77 | ui 78 | JIRA-\d+ 79 | # Configure that a scope must always be provided. 80 | requireScope: true 81 | # Configure which scopes are disallowed in PR titles (newline-delimited). 82 | # For instance by setting the value below, `chore(release): ...` (lowercase) 83 | # and `ci(e2e,release): ...` (unknown scope) will be rejected. 84 | # These are regex patterns auto-wrapped in `^ $`. 85 | disallowScopes: | 86 | release 87 | [A-Z]+ 88 | # Configure additional validation for the subject based on a regex. 89 | # This example ensures the subject doesn't start with an uppercase character. 90 | subjectPattern: ^(?![A-Z]).+$ 91 | # If `subjectPattern` is configured, you can use this property to override 92 | # the default error message that is shown when the pattern doesn't match. 93 | # The variables `subject` and `title` can be used within the message. 94 | subjectPatternError: | 95 | The subject "{subject}" found in the pull request title "{title}" 96 | didn't match the configured pattern. Please ensure that the subject 97 | doesn't start with an uppercase character. 98 | # The GitHub base URL will be automatically set to the correct value from the GitHub context variable. 99 | # If you want to override this, you can do so here (not recommended). 100 | githubBaseUrl: https://github.myorg.com/api/v3 101 | # If the PR contains one of these newline-delimited labels, the 102 | # validation is skipped. If you want to rerun the validation when 103 | # labels change, you might want to use the `labeled` and `unlabeled` 104 | # event triggers in your workflow. 105 | ignoreLabels: | 106 | bot 107 | ignore-semantic-pull-request 108 | # If you're using a format for the PR title that differs from the traditional Conventional 109 | # Commits spec, you can use these options to customize the parsing of the type, scope and 110 | # subject. The `headerPattern` should contain a regex where the capturing groups in parentheses 111 | # correspond to the parts listed in `headerPatternCorrespondence`. 112 | # See: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern 113 | headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$' 114 | headerPatternCorrespondence: type, scope, subject 115 | ``` 116 | 117 | ### Work-in-progress pull requests 118 | 119 | For work-in-progress PRs you can typically use [draft pull requests from GitHub](https://github.blog/2019-02-14-introducing-draft-pull-requests/). However, private repositories on the free plan don't have this option and therefore this action allows you to opt-in to using the special "[WIP]" prefix to indicate this state. 120 | 121 | **Example:** 122 | 123 | ``` 124 | [WIP] feat: Add support for Node.js 18 125 | ``` 126 | 127 | This will prevent the PR title from being validated, and pull request checks will remain pending. 128 | 129 | **Attention**: If you want to use the this feature, you need to grant the `pull-requests: write` permission to the GitHub Action. This is because the action will update the status of the PR to remain in a pending state while `[WIP]` is present in the PR title. 130 | 131 | ```yml 132 | name: 'Lint PR' 133 | 134 | permissions: 135 | pull-requests: write 136 | 137 | jobs: 138 | main: 139 | name: Validate PR title 140 | runs-on: ubuntu-latest 141 | permissions: 142 | pull-requests: read 143 | steps: 144 | - uses: amannn/action-semantic-pull-request@v6 145 | env: 146 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 147 | with: 148 | wip: true 149 | ``` 150 | 151 | ### Legacy configuration for validating single commits 152 | 153 | When using "Squash and merge" on a PR with only one commit, GitHub will suggest using that commit message instead of the PR title for the merge commit. As it's easy to commit this by mistake this action supports two configuration options to provide additional validation for this case. 154 | 155 | ```yml 156 | # If the PR only contains a single commit, the action will validate that 157 | # it matches the configured pattern. 158 | validateSingleCommit: true 159 | # Related to `validateSingleCommit` you can opt-in to validate that the PR 160 | # title matches a single commit to avoid confusion. 161 | validateSingleCommitMatchesPrTitle: true 162 | ``` 163 | 164 | However, [GitHub has introduced an option to streamline this behaviour](https://github.blog/changelog/2022-05-11-default-to-pr-titles-for-squash-merge-commit-messages/), so using that instead should be preferred. 165 | 166 | ## Event triggers 167 | 168 | There are two events that can be used as triggers for this action, each with different characteristics: 169 | 170 | 1. [`pull_request_target`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target): This allows the action to be used in a fork-based workflow, where e.g. you want to accept pull requests in a public repository. In this case, the configuration from the main branch of your repository will be used for the check. This means that you need to have this configuration in the main branch for the action to run at all (e.g. it won't run within a PR that adds the action initially). Also if you change the configuration in a PR, the changes will not be reflected for the current PR – only subsequent ones after the changes are in the main branch. 171 | 2. [`pull_request`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request): This configuration uses the latest configuration that is available in the current branch. It will only work if the branch is based in the repository itself. If this configuration is used and a pull request from a fork is opened, you'll encounter an error as the GitHub token environment parameter is not available. This option is viable if all contributors have write access to the repository. 172 | 173 | > [!TIP] 174 | > If the workflow is required for merging, you need to ensure that the you add a trigger type for [`synchronize`](https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=synchronize#pull_request). 175 | > This will ensure that the check is ran on each new push as required workflows need to run on the lastest change. 176 | 177 | ## Outputs 178 | 179 | - The outputs `type`, `scope` and `subject` are populated, except for if the `wip` option is used. 180 | - The `error_message` output will be populated in case the validation fails. 181 | 182 | [An output can be used in other steps](https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs), for example to comment the error message onto the pull request. 183 | 184 |
185 | Example 186 | 187 | ````yml 188 | name: 'Lint PR' 189 | 190 | on: 191 | pull_request_target: 192 | types: 193 | - opened 194 | - edited 195 | 196 | permissions: 197 | pull-requests: write 198 | 199 | jobs: 200 | main: 201 | name: Validate PR title 202 | runs-on: ubuntu-latest 203 | permissions: 204 | pull-requests: read 205 | steps: 206 | - uses: amannn/action-semantic-pull-request@v6 207 | id: lint_pr_title 208 | env: 209 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 210 | 211 | - uses: marocchino/sticky-pull-request-comment@v2 212 | # When the previous steps fails, the workflow would stop. By adding this 213 | # condition you can continue the execution with the populated error message. 214 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 215 | with: 216 | header: pr-title-lint-error 217 | message: | 218 | Hey there and thank you for opening this pull request! 👋🏼 219 | 220 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 221 | 222 | Details: 223 | 224 | ``` 225 | ${{ steps.lint_pr_title.outputs.error_message }} 226 | ``` 227 | 228 | # Delete a previous comment when the issue has been resolved 229 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 230 | uses: marocchino/sticky-pull-request-comment@v2 231 | with: 232 | header: pr-title-lint-error 233 | delete: true 234 | ```` 235 | 236 |
237 | -------------------------------------------------------------------------------- /src/validatePrTitle.test.js: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest'; 2 | import validatePrTitle from './validatePrTitle.js'; 3 | 4 | it('allows valid PR titles that use the default types', async () => { 5 | const inputs = [ 6 | 'fix: Fix bug', 7 | 'fix!: Fix bug', 8 | 'feat: Add feature', 9 | 'feat!: Add feature', 10 | 'refactor: Internal cleanup' 11 | ]; 12 | 13 | for (let index = 0; index < inputs.length; index++) { 14 | await validatePrTitle(inputs[index]); 15 | } 16 | }); 17 | 18 | it('throws for PR titles without a type', async () => { 19 | await expect(validatePrTitle('Fix bug')).rejects.toThrow( 20 | 'No release type found in pull request title "Fix bug".' 21 | ); 22 | }); 23 | 24 | it('throws for PR titles with only a type', async () => { 25 | await expect(validatePrTitle('fix:')).rejects.toThrow( 26 | 'No release type found in pull request title "fix:".' 27 | ); 28 | }); 29 | 30 | it('throws for PR titles without a subject', async () => { 31 | await expect(validatePrTitle('fix: ')).rejects.toThrow( 32 | 'No subject found in pull request title "fix: ".' 33 | ); 34 | }); 35 | 36 | it('throws for PR titles with an unknown type', async () => { 37 | await expect(validatePrTitle('foo: Bar')).rejects.toThrow( 38 | 'Unknown release type "foo" found in pull request title "foo: Bar".' 39 | ); 40 | }); 41 | 42 | describe('regex types', () => { 43 | const headerPattern = /^([\w-]*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$/; 44 | 45 | it('allows a regex matching type', async () => { 46 | await validatePrTitle('JIRA-123: Bar', { 47 | types: ['JIRA-\\d+'], 48 | headerPattern 49 | }); 50 | }); 51 | 52 | it('can be used for dynamic Jira keys', async () => { 53 | const inputs = ['JIRA-123', 'P-123', 'INT-31', 'CONF-0']; 54 | 55 | for (let index = 0; index < inputs.length; index++) { 56 | await validatePrTitle(`${inputs[index]}: did the thing`, { 57 | types: ['[A-Z]+-\\d+'], 58 | headerPattern 59 | }); 60 | } 61 | }); 62 | 63 | it('throws for PR titles without a type', async () => { 64 | await expect( 65 | validatePrTitle('Fix JIRA-123 bug', { 66 | types: ['JIRA-\\d+'], 67 | headerPattern 68 | }) 69 | ).rejects.toThrow( 70 | 'No release type found in pull request title "Fix JIRA-123 bug".' 71 | ); 72 | }); 73 | 74 | it('throws for PR titles with only a type', async () => { 75 | await expect( 76 | validatePrTitle('JIRA-123:', { 77 | types: ['JIRA-\\d+'], 78 | headerPattern 79 | }) 80 | ).rejects.toThrow( 81 | 'No release type found in pull request title "JIRA-123:".' 82 | ); 83 | }); 84 | 85 | it('throws for PR titles without a subject', async () => { 86 | await expect( 87 | validatePrTitle('JIRA-123: ', { 88 | types: ['JIRA-\\d+'], 89 | headerPattern 90 | }) 91 | ).rejects.toThrow('No subject found in pull request title "JIRA-123: ".'); 92 | }); 93 | 94 | it('throws for PR titles that do not match the regex', async () => { 95 | await expect( 96 | validatePrTitle('CONF-123: ', { 97 | types: ['JIRA-\\d+'], 98 | headerPattern 99 | }) 100 | ).rejects.toThrow('No subject found in pull request title "CONF-123: ".'); 101 | }); 102 | 103 | it('throws when an unknown type is detected for auto-wrapping regex', async () => { 104 | await expect( 105 | validatePrTitle('JIRA-123A: Bar', { 106 | types: ['JIRA-\\d+'], 107 | headerPattern 108 | }) 109 | ).rejects.toThrow( 110 | 'Unknown release type "JIRA-123A" found in pull request title "JIRA-123A: Bar".\n\nAvailable types:\n - JIRA-\\d+' 111 | ); 112 | }); 113 | 114 | it('allows scopes when using a regex for the type', async () => { 115 | await validatePrTitle('JIRA-123(core): Bar', { 116 | types: ['JIRA-\\d+'], 117 | headerPattern 118 | }); 119 | }); 120 | }); 121 | 122 | describe('defined scopes', () => { 123 | it('allows a missing scope by default', async () => { 124 | await validatePrTitle('fix: Bar'); 125 | }); 126 | 127 | it('allows all scopes by default', async () => { 128 | await validatePrTitle('fix(core): Bar'); 129 | }); 130 | 131 | it('allows a missing scope when custom scopes are defined', async () => { 132 | await validatePrTitle('fix: Bar', {scopes: ['foo']}); 133 | }); 134 | 135 | it('allows a matching scope', async () => { 136 | await validatePrTitle('fix(core): Bar', {scopes: ['core']}); 137 | }); 138 | 139 | it('allows a regex matching scope', async () => { 140 | await validatePrTitle('fix(CORE): Bar', {scopes: ['[A-Z]+']}); 141 | }); 142 | 143 | it('allows multiple matching scopes', async () => { 144 | await validatePrTitle('fix(core,e2e): Bar', { 145 | scopes: ['core', 'e2e', 'web'] 146 | }); 147 | }); 148 | 149 | it('allows multiple regex matching scopes', async () => { 150 | await validatePrTitle('fix(CORE,WEB): Bar', { 151 | scopes: ['[A-Z]+'] 152 | }); 153 | }); 154 | 155 | it('throws when an unknown scope is detected within multiple scopes', async () => { 156 | await expect( 157 | validatePrTitle('fix(core,e2e,foo,bar): Bar', {scopes: ['foo', 'core']}) 158 | ).rejects.toThrow( 159 | 'Unknown scopes "e2e,bar" found in pull request title "fix(core,e2e,foo,bar): Bar". Scope must match one of: foo, core.' 160 | ); 161 | }); 162 | 163 | it('throws when an unknown scope is detected within multiple scopes and a regex', async () => { 164 | await expect( 165 | validatePrTitle('fix(CORE,e2e,foo,bar): Bar', { 166 | scopes: ['foo', '[A-Z]+'] 167 | }) 168 | ).rejects.toThrow( 169 | 'Unknown scopes "e2e,bar" found in pull request title "fix(CORE,e2e,foo,bar): Bar". Scope must match one of: foo, [A-Z]+.' 170 | ); 171 | }); 172 | 173 | it('throws when an unknown scope is detected', async () => { 174 | await expect( 175 | validatePrTitle('fix(core): Bar', {scopes: ['foo']}) 176 | ).rejects.toThrow( 177 | 'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: foo.' 178 | ); 179 | }); 180 | 181 | it('throws when an unknown scope is detected for auto-wrapped regex matching', async () => { 182 | await expect( 183 | validatePrTitle('fix(score): Bar', {scopes: ['core']}) 184 | ).rejects.toThrow( 185 | 'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: core.' 186 | ); 187 | }); 188 | 189 | it('throws when an unknown scope is detected for auto-wrapped regex matching when input is already wrapped', async () => { 190 | await expect( 191 | validatePrTitle('fix(score): Bar', {scopes: ['^[A-Z]+$']}) 192 | ).rejects.toThrow( 193 | 'Unknown scope "score" found in pull request title "fix(score): Bar". Scope must match one of: ^[A-Z]+$.' 194 | ); 195 | }); 196 | 197 | it('throws when an unknown scope is detected for regex matching', async () => { 198 | await expect( 199 | validatePrTitle('fix(core): Bar', {scopes: ['[A-Z]+']}) 200 | ).rejects.toThrow( 201 | 'Unknown scope "core" found in pull request title "fix(core): Bar". Scope must match one of: [A-Z]+.' 202 | ); 203 | }); 204 | 205 | describe('require scope', () => { 206 | describe('scope allowlist defined', () => { 207 | it('passes when a scope is provided', async () => { 208 | await validatePrTitle('fix(core): Bar', { 209 | scopes: ['core'], 210 | requireScope: true 211 | }); 212 | }); 213 | 214 | it('throws when a scope is missing', async () => { 215 | await expect( 216 | validatePrTitle('fix: Bar', { 217 | scopes: ['foo', 'bar'], 218 | requireScope: true 219 | }) 220 | ).rejects.toThrow( 221 | 'No scope found in pull request title "fix: Bar". Scope must match one of: foo, bar.' 222 | ); 223 | }); 224 | }); 225 | 226 | describe('disallow scopes', () => { 227 | it('passes when a single scope is provided, but not present in disallowScopes with one item', async () => { 228 | await validatePrTitle('fix(core): Bar', {disallowScopes: ['release']}); 229 | }); 230 | 231 | it('passes when a single scope is provided, but not present in disallowScopes with one regex item', async () => { 232 | await validatePrTitle('fix(core): Bar', {disallowScopes: ['[A-Z]+']}); 233 | }); 234 | 235 | it('passes when multiple scopes are provided, but not present in disallowScopes with one item', async () => { 236 | await validatePrTitle('fix(core,e2e,bar): Bar', { 237 | disallowScopes: ['release'] 238 | }); 239 | }); 240 | 241 | it('passes when multiple scopes are provided, but not present in disallowScopes with one regex item', async () => { 242 | await validatePrTitle('fix(core,e2e,bar): Bar', { 243 | disallowScopes: ['[A-Z]+'] 244 | }); 245 | }); 246 | 247 | it('passes when a single scope is provided, but not present in disallowScopes with multiple items', async () => { 248 | await validatePrTitle('fix(core): Bar', { 249 | disallowScopes: ['release', 'test', '[A-Z]+'] 250 | }); 251 | }); 252 | 253 | it('passes when multiple scopes are provided, but not present in disallowScopes with multiple items', async () => { 254 | await validatePrTitle('fix(core,e2e,bar): Bar', { 255 | disallowScopes: ['release', 'test', '[A-Z]+'] 256 | }); 257 | }); 258 | 259 | it('throws when a single scope is provided and it is present in disallowScopes with one item', async () => { 260 | await expect( 261 | validatePrTitle('fix(release): Bar', {disallowScopes: ['release']}) 262 | ).rejects.toThrow('Disallowed scope was found: release'); 263 | }); 264 | 265 | it('throws when a single scope is provided and it is present in disallowScopes with one regex item', async () => { 266 | await expect( 267 | validatePrTitle('fix(RELEASE): Bar', {disallowScopes: ['[A-Z]+']}) 268 | ).rejects.toThrow('Disallowed scope was found: RELEASE'); 269 | }); 270 | 271 | it('throws when a single scope is provided and it is present in disallowScopes with multiple item', async () => { 272 | await expect( 273 | validatePrTitle('fix(release): Bar', { 274 | disallowScopes: ['release', 'test'] 275 | }) 276 | ).rejects.toThrow('Disallowed scope was found: release'); 277 | }); 278 | 279 | it('throws when a single scope is provided and it is present in disallowScopes with multiple regex item', async () => { 280 | await expect( 281 | validatePrTitle('fix(RELEASE): Bar', { 282 | disallowScopes: ['[A-Z]+', '^[A-Z].+$'] 283 | }) 284 | ).rejects.toThrow('Disallowed scope was found: RELEASE'); 285 | }); 286 | 287 | it('throws when multiple scopes are provided and one of them is present in disallowScopes with one item ', async () => { 288 | await expect( 289 | validatePrTitle('fix(release,e2e): Bar', { 290 | disallowScopes: ['release'] 291 | }) 292 | ).rejects.toThrow('Disallowed scope was found: release'); 293 | }); 294 | 295 | it('throws when multiple scopes are provided and one of them is present in disallowScopes with one regex item ', async () => { 296 | await expect( 297 | validatePrTitle('fix(RELEASE,e2e): Bar', { 298 | disallowScopes: ['[A-Z]+'] 299 | }) 300 | ).rejects.toThrow('Disallowed scope was found: RELEASE'); 301 | }); 302 | 303 | it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items ', async () => { 304 | await expect( 305 | validatePrTitle('fix(release,e2e): Bar', { 306 | disallowScopes: ['release', 'test'] 307 | }) 308 | ).rejects.toThrow('Disallowed scope was found: release'); 309 | }); 310 | 311 | it('throws when multiple scopes are provided and one of them is present in disallowScopes with multiple items and a regex', async () => { 312 | await expect( 313 | validatePrTitle('fix(RELEASE,e2e): Bar', { 314 | disallowScopes: ['[A-Z]+', 'test'] 315 | }) 316 | ).rejects.toThrow('Disallowed scope was found: RELEASE'); 317 | }); 318 | 319 | it('throws when multiple scopes are provided and more than one of them are present in disallowScopes', async () => { 320 | await expect( 321 | validatePrTitle('fix(release,test,CORE): Bar', { 322 | disallowScopes: ['release', 'test', '[A-Z]+'] 323 | }) 324 | ).rejects.toThrow('Disallowed scopes were found: release, test, CORE'); 325 | }); 326 | }); 327 | 328 | describe('scope allowlist not defined', () => { 329 | it('passes when a scope is provided', async () => { 330 | await validatePrTitle('fix(core): Bar', { 331 | requireScope: true 332 | }); 333 | }); 334 | 335 | it('throws when a scope is missing', async () => { 336 | await expect( 337 | validatePrTitle('fix: Bar', { 338 | requireScope: true 339 | }) 340 | ).rejects.toThrow( 341 | // Should make no mention of any available scope 342 | /^No scope found in pull request title "fix: Bar".$/ 343 | ); 344 | }); 345 | }); 346 | }); 347 | }); 348 | 349 | describe('custom types', () => { 350 | it('allows PR titles with a supported type', async () => { 351 | const inputs = ['foo: Foobar', 'bar: Foobar', 'baz: Foobar']; 352 | const types = ['foo', 'bar', 'baz']; 353 | 354 | for (let index = 0; index < inputs.length; index++) { 355 | await validatePrTitle(inputs[index], {types}); 356 | } 357 | }); 358 | 359 | it('throws for PR titles with an unknown type', async () => { 360 | await expect( 361 | validatePrTitle('fix: Foobar', {types: ['foo', 'bar']}) 362 | ).rejects.toThrow( 363 | 'Unknown release type "fix" found in pull request title "fix: Foobar".' 364 | ); 365 | }); 366 | }); 367 | 368 | describe('description validation', () => { 369 | it('does not validate the description by default', async () => { 370 | await validatePrTitle('fix: sK!"§4123'); 371 | }); 372 | 373 | it('can pass the validation when `subjectPatternError` is configured', async () => { 374 | await validatePrTitle('fix: foobar', { 375 | subjectPattern: '^(?![A-Z]).+$', 376 | subjectPatternError: 377 | 'The subject found in the pull request title cannot start with an uppercase character.' 378 | }); 379 | }); 380 | 381 | it('uses the `subjectPatternError` if available when the `subjectPattern` does not match', async () => { 382 | const customError = 383 | 'The subject found in the pull request title cannot start with an uppercase character.'; 384 | await expect( 385 | validatePrTitle('fix: Foobar', { 386 | subjectPattern: '^(?![A-Z]).+$', 387 | subjectPatternError: customError 388 | }) 389 | ).rejects.toThrow(customError); 390 | }); 391 | 392 | it('interpolates variables into `subjectPatternError`', async () => { 393 | await expect( 394 | validatePrTitle('fix: Foobar', { 395 | subjectPattern: '^(?![A-Z]).+$', 396 | subjectPatternError: 397 | 'The subject "{subject}" found in the pull request title "{title}" cannot start with an uppercase character.' 398 | }) 399 | ).rejects.toThrow( 400 | 'The subject "Foobar" found in the pull request title "fix: Foobar" cannot start with an uppercase character.' 401 | ); 402 | }); 403 | 404 | it('throws for invalid subjects', async () => { 405 | await expect( 406 | validatePrTitle('fix: Foobar', { 407 | subjectPattern: '^(?![A-Z]).+$' 408 | }) 409 | ).rejects.toThrow( 410 | 'The subject "Foobar" found in pull request title "fix: Foobar" doesn\'t match the configured pattern "^(?![A-Z]).+$".' 411 | ); 412 | }); 413 | 414 | it('throws for only partial matches', async () => { 415 | await expect( 416 | validatePrTitle('fix: Foobar', { 417 | subjectPattern: 'Foo' 418 | }) 419 | ).rejects.toThrow( 420 | 'The subject "Foobar" found in pull request title "fix: Foobar" isn\'t an exact match for the configured pattern "Foo". Please provide a subject that matches the whole pattern exactly.' 421 | ); 422 | }); 423 | 424 | it('accepts valid subjects', async () => { 425 | await validatePrTitle('fix: foobar', { 426 | subjectPattern: '^(?![A-Z]).+$' 427 | }); 428 | }); 429 | }); 430 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [6.1.1](https://github.com/amannn/action-semantic-pull-request/compare/v6.1.0...v6.1.1) (2025-08-22) 4 | 5 | ### Bug Fixes 6 | 7 | * Parse `headerPatternCorrespondence` properly ([#295](https://github.com/amannn/action-semantic-pull-request/issues/295)) ([800da4c](https://github.com/amannn/action-semantic-pull-request/commit/800da4c97f618e44f972ff9bc21ab5daecc97773)) 8 | 9 | ## [6.1.0](https://github.com/amannn/action-semantic-pull-request/compare/v6.0.1...v6.1.0) (2025-08-19) 10 | 11 | ### Features 12 | 13 | * Support providing regexps for types ([#292](https://github.com/amannn/action-semantic-pull-request/issues/292)) ([a30288b](https://github.com/amannn/action-semantic-pull-request/commit/a30288bf13b78cca17c3abdc144db5977476fc8b)) 14 | 15 | ### Bug Fixes 16 | 17 | * Remove trailing whitespace from "unknown release type" error message ([#291](https://github.com/amannn/action-semantic-pull-request/issues/291)) ([afa4edb](https://github.com/amannn/action-semantic-pull-request/commit/afa4edb1c465fb22230da8ff4776a163ab5facdf)) 18 | 19 | ## [6.0.1](https://github.com/amannn/action-semantic-pull-request/compare/v6.0.0...v6.0.1) (2025-08-13) 20 | 21 | ### Bug Fixes 22 | 23 | * Actually execute action ([#289](https://github.com/amannn/action-semantic-pull-request/issues/289)) ([58e4ab4](https://github.com/amannn/action-semantic-pull-request/commit/58e4ab40f59be79f2c432bf003e34a31174e977a)) 24 | 25 | ## [6.0.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.5.3...v6.0.0) (2025-08-13) 26 | 27 | ### ⚠ BREAKING CHANGES 28 | 29 | * Upgrade action to use Node.js 24 and ESM (#287) 30 | 31 | ### Features 32 | 33 | * Upgrade action to use Node.js 24 and ESM ([#287](https://github.com/amannn/action-semantic-pull-request/issues/287)) ([bc0c9a7](https://github.com/amannn/action-semantic-pull-request/commit/bc0c9a79abfe07c0f08c498dd4a040bd22fe9b79)) 34 | 35 | ## [5.5.3](https://github.com/amannn/action-semantic-pull-request/compare/v5.5.2...v5.5.3) (2024-06-28) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * Bump `braces` dependency ([#269](https://github.com/amannn/action-semantic-pull-request/issues/269). by @EelcoLos) ([2d952a1](https://github.com/amannn/action-semantic-pull-request/commit/2d952a1bf90a6a7ab8f0293dc86f5fdf9acb1915)) 41 | 42 | ## [5.5.2](https://github.com/amannn/action-semantic-pull-request/compare/v5.5.1...v5.5.2) (2024-04-24) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * Bump tar from 6.1.11 to 6.2.1 ([#262](https://github.com/amannn/action-semantic-pull-request/issues/262) by @EelcoLos) ([9a90d5a](https://github.com/amannn/action-semantic-pull-request/commit/9a90d5a5ac979326e3bb9272750cdd4f192ce24a)) 48 | 49 | ## [5.5.1](https://github.com/amannn/action-semantic-pull-request/compare/v5.5.0...v5.5.1) (2024-04-24) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * Bump ip from 2.0.0 to 2.0.1 ([#263](https://github.com/amannn/action-semantic-pull-request/issues/263) by @EelcoLos) ([5e7e9ac](https://github.com/amannn/action-semantic-pull-request/commit/5e7e9acca3ddc6a9d7b640fe1f905c4fff131f4a)) 55 | 56 | ## [5.5.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.4.0...v5.5.0) (2024-04-23) 57 | 58 | 59 | ### Features 60 | 61 | * Add outputs for `type`, `scope` and `subject` ([#261](https://github.com/amannn/action-semantic-pull-request/issues/261) by [@bcaurel](https://github.com/bcaurel)) ([b05f5f6](https://github.com/amannn/action-semantic-pull-request/commit/b05f5f6423ef5cdfc7fdff00c4c10dd9a4f54aff)) 62 | 63 | ## [5.4.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.3.0...v5.4.0) (2023-11-03) 64 | 65 | 66 | ### Features 67 | 68 | * Use `github.api_url` as default for `githubBaseUrl` ([#243](https://github.com/amannn/action-semantic-pull-request/issues/243) by [@fty4](https://github.com/fty4)) ([4d5734a](https://github.com/amannn/action-semantic-pull-request/commit/4d5734a0a29e548daecc9e7bfeb9bb8b3acdee1e)) 69 | 70 | ## [5.3.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.2.0...v5.3.0) (2023-09-25) 71 | 72 | 73 | ### Features 74 | 75 | * Use Node.js 20 in action ([#240](https://github.com/amannn/action-semantic-pull-request/issues/240)) ([4c0d5a2](https://github.com/amannn/action-semantic-pull-request/commit/4c0d5a21fc86635c67cc57ffe89d842c34ade284)) 76 | 77 | ## [5.2.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.1.0...v5.2.0) (2023-03-16) 78 | 79 | 80 | ### Features 81 | 82 | * Update dependencies by @EelcoLos ([#229](https://github.com/amannn/action-semantic-pull-request/issues/229)) ([e797448](https://github.com/amannn/action-semantic-pull-request/commit/e797448a07516738bcfdd6f26ad1d1f84c58d0cc)) 83 | 84 | ## [5.1.0](https://github.com/amannn/action-semantic-pull-request/compare/v5.0.2...v5.1.0) (2023-02-10) 85 | 86 | 87 | ### Features 88 | 89 | * Add regex support to `scope` and `disallowScopes` configuration ([#226](https://github.com/amannn/action-semantic-pull-request/issues/226)) ([403a6f8](https://github.com/amannn/action-semantic-pull-request/commit/403a6f89242a0d0d3acde94e6141b2e0f4da8838)) 90 | 91 | ### [5.0.2](https://github.com/amannn/action-semantic-pull-request/compare/v5.0.1...v5.0.2) (2022-10-17) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * Upgrade `@actions/core` to avoid deprecation warnings ([#208](https://github.com/amannn/action-semantic-pull-request/issues/208)) ([91f4126](https://github.com/amannn/action-semantic-pull-request/commit/91f4126c9e8625b9cadd64b02a03018fa22fc498)) 97 | 98 | ### [5.0.1](https://github.com/amannn/action-semantic-pull-request/compare/v5.0.0...v5.0.1) (2022-10-14) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * Upgrade GitHub Action to use Node v16 ([#207](https://github.com/amannn/action-semantic-pull-request/issues/207)) ([6282ee3](https://github.com/amannn/action-semantic-pull-request/commit/6282ee339b067cb8eab05026f91153f873ad37fb)) 104 | 105 | ## [5.0.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.6.0...v5.0.0) (2022-10-11) 106 | 107 | 108 | ### ⚠ BREAKING CHANGES 109 | 110 | * Enum options need to be newline delimited (to allow whitespace within them) (#205) 111 | 112 | ### Features 113 | 114 | * Enum options need to be newline delimited (to allow whitespace within them) ([#205](https://github.com/amannn/action-semantic-pull-request/issues/205)) ([c906fe1](https://github.com/amannn/action-semantic-pull-request/commit/c906fe1e5a4bcc61624931ca94da9672107bd448)) 115 | 116 | ## [4.6.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.5.0...v4.6.0) (2022-09-26) 117 | 118 | 119 | ### Features 120 | 121 | * Provide error messages as `outputs.error_message` ([#194](https://github.com/amannn/action-semantic-pull-request/issues/194)) ([880a3c0](https://github.com/amannn/action-semantic-pull-request/commit/880a3c061c0dea01e977cefe26fb0e0d06b3d1a9)) 122 | 123 | ## [4.5.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.4.0...v4.5.0) (2022-05-04) 124 | 125 | 126 | ### Features 127 | 128 | * Add `disallowScopes` option ([#179](https://github.com/amannn/action-semantic-pull-request/issues/179)) ([6a7ed2d](https://github.com/amannn/action-semantic-pull-request/commit/6a7ed2d5046cf8a40c60494c83c962343061874a)) 129 | 130 | ## [4.4.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.3.0...v4.4.0) (2022-04-22) 131 | 132 | 133 | ### Features 134 | 135 | * Add options to pass custom regex to conventional-commits-parser ([#177](https://github.com/amannn/action-semantic-pull-request/issues/177)) ([956659a](https://github.com/amannn/action-semantic-pull-request/commit/956659ae00eaa0b00fe5a58dfdf3a3db1efd1d63)) 136 | 137 | ## [4.3.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.2.0...v4.3.0) (2022-04-13) 138 | 139 | 140 | ### Features 141 | 142 | * Add `ignoreLabels` option to opt-out of validation for certain PRs ([#174](https://github.com/amannn/action-semantic-pull-request/issues/174)) ([277c230](https://github.com/amannn/action-semantic-pull-request/commit/277c2303f965680aed7613eb512365c58aa92b6b)) 143 | 144 | ## [4.2.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.1.0...v4.2.0) (2022-02-08) 145 | 146 | 147 | ### Features 148 | 149 | * Add opt-in validation that PR titles match a single commit ([#160](https://github.com/amannn/action-semantic-pull-request/issues/160)) ([c05e358](https://github.com/amannn/action-semantic-pull-request/commit/c05e3587cb7878ec080300180d31d61ba1cf01ea)) 150 | 151 | ## [4.1.0](https://github.com/amannn/action-semantic-pull-request/compare/v4.0.1...v4.1.0) (2022-02-04) 152 | 153 | 154 | ### Features 155 | 156 | * Check if the PR title matches the commit title when single commits are validated to avoid surprises ([#158](https://github.com/amannn/action-semantic-pull-request/issues/158)) ([f1216e9](https://github.com/amannn/action-semantic-pull-request/commit/f1216e9607ae4b476a6584a899c39bbb4f62da6d)) 157 | 158 | ### [4.0.1](https://github.com/amannn/action-semantic-pull-request/compare/v4.0.0...v4.0.1) (2022-02-03) 159 | 160 | 161 | ### Bug Fixes 162 | 163 | * Upgrade dependencies ([#156](https://github.com/amannn/action-semantic-pull-request/issues/156)) ([16c6cc6](https://github.com/amannn/action-semantic-pull-request/commit/16c6cc670bd7e91dbcfd9c39de6e6436d2c0fe1b)) 164 | 165 | ## [4.0.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.7.0...v4.0.0) (2022-02-02) 166 | 167 | 168 | ### ⚠ BREAKING CHANGES 169 | 170 | * dropped support for node <=15 171 | 172 | ### Features 173 | 174 | * Upgrade semantic-release@19.0.2 ([#155](https://github.com/amannn/action-semantic-pull-request/issues/155)) ([ca264e0](https://github.com/amannn/action-semantic-pull-request/commit/ca264e08ba87f01cd802533512d9787d07a5ba98)) 175 | 176 | ## [3.7.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.6.0...v3.7.0) (2022-02-02) 177 | 178 | 179 | ### Features 180 | 181 | * Upgrade @actions/github ([#154](https://github.com/amannn/action-semantic-pull-request/issues/154)) ([c85a868](https://github.com/amannn/action-semantic-pull-request/commit/c85a868a5178060d1a5abcea37546d403b923e6c)) 182 | 183 | ## [3.6.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.5.0...v3.6.0) (2022-01-05) 184 | 185 | 186 | ### Features 187 | 188 | * Publish major version tag ([c47e831](https://github.com/amannn/action-semantic-pull-request/commit/c47e8318667a1e17cbe7132cea17eaf5d06cc403)) 189 | 190 | ## [3.5.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.6...v3.5.0) (2021-12-15) 191 | 192 | 193 | ### Features 194 | 195 | * Add support for Github Enterprise ([#145](https://github.com/amannn/action-semantic-pull-request/issues/145)) ([579fb11](https://github.com/amannn/action-semantic-pull-request/commit/579fb115c050f156ee6d52244a7ff41b685a89fd)) 196 | 197 | ### [3.4.6](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.5...v3.4.6) (2021-10-31) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * Better strategy to detect merge commits ([#132](https://github.com/amannn/action-semantic-pull-request/issues/132)) ([f913d37](https://github.com/amannn/action-semantic-pull-request/commit/f913d374b7bc698a5831a12c8955d1373c439548)) 203 | 204 | ### [3.4.5](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.4...v3.4.5) (2021-10-28) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * Consider merge commits for single commit validation ([#131](https://github.com/amannn/action-semantic-pull-request/issues/131)) ([5265383](https://github.com/amannn/action-semantic-pull-request/commit/526538350f2e4eaaac841e586a197de6b019af1f)), closes [#108](https://github.com/amannn/action-semantic-pull-request/issues/108) 210 | 211 | ### [3.4.4](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.3...v3.4.4) (2021-10-26) 212 | 213 | 214 | ### Reverts 215 | 216 | * Revert "fix: Consider merge commits for single commit validation (#127)" ([d40b0d4](https://github.com/amannn/action-semantic-pull-request/commit/d40b0d425a054807cc5e032a827cc5780f507630)), closes [#127](https://github.com/amannn/action-semantic-pull-request/issues/127) 217 | 218 | ### [3.4.3](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.2...v3.4.3) (2021-10-26) 219 | 220 | 221 | ### Bug Fixes 222 | 223 | * Consider merge commits for single commit validation ([#127](https://github.com/amannn/action-semantic-pull-request/issues/127)) ([1b347f7](https://github.com/amannn/action-semantic-pull-request/commit/1b347f79d6518f5d0190913abf7815286f490f53)), closes [#108](https://github.com/amannn/action-semantic-pull-request/issues/108) [#108](https://github.com/amannn/action-semantic-pull-request/issues/108) 224 | 225 | ### [3.4.2](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.1...v3.4.2) (2021-08-16) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * Don't require `scopes` when `requireScope` enabled ([#114](https://github.com/amannn/action-semantic-pull-request/issues/114)) ([4c0fe2f](https://github.com/amannn/action-semantic-pull-request/commit/4c0fe2f50d26390ef6a2553a01d9bd13bef2caf2)) 231 | 232 | ### [3.4.1](https://github.com/amannn/action-semantic-pull-request/compare/v3.4.0...v3.4.1) (2021-07-26) 233 | 234 | 235 | ### Bug Fixes 236 | 237 | * Make link in error message clickable ([#112](https://github.com/amannn/action-semantic-pull-request/issues/112)) ([2a292a6](https://github.com/amannn/action-semantic-pull-request/commit/2a292a6550224ddc9d79731bcd15732b42344ebf)) 238 | 239 | ## [3.4.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.3.0...v3.4.0) (2021-02-15) 240 | 241 | 242 | ### Features 243 | 244 | * Add `validateSingleCommit` flag to validate the commit message if there's only a single commit in the PR (opt-in). This is intended to be used as a workaround for an issue with Github as in this case, the PR title won't be used as the default commit message when merging a PR. ([#87](https://github.com/amannn/action-semantic-pull-request/issues/87)) ([3f20459](https://github.com/amannn/action-semantic-pull-request/commit/3f20459aa1121f2f0093f06f565a95fe7c5cf402)) 245 | 246 | ## [3.3.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.2.6...v3.3.0) (2021-02-10) 247 | 248 | 249 | ### Features 250 | 251 | * Add ability to use multiple scopes ([#85](https://github.com/amannn/action-semantic-pull-request/issues/85)) ([d6aabb6](https://github.com/amannn/action-semantic-pull-request/commit/d6aabb6fedc5f57cec60b16db8595a92f1e263ab)) 252 | 253 | ### [3.2.6](https://github.com/amannn/action-semantic-pull-request/compare/v3.2.5...v3.2.6) (2021-01-25) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * Publish changelog ([47ac28b](https://github.com/amannn/action-semantic-pull-request/commit/47ac28b008b9b34b6cda0c1d558f4b8f25a0d3bb)) 259 | 260 | ### [3.2.1](https://github.com/amannn/action-semantic-pull-request/compare/v3.2.0...v3.2.1) (2021-01-19) 261 | 262 | 263 | ### Bug Fixes 264 | 265 | * Move configuration docs to a separate section ([dd78147](https://github.com/amannn/action-semantic-pull-request/commit/dd78147b453899371b7406672fb5ebe9dc5e2c5f)) 266 | 267 | ## [3.2.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.1.0...v3.2.0) (2021-01-18) 268 | 269 | 270 | ### Features 271 | 272 | * Add `subjectPatternError` option to allow users to override the default error when `subjectPattern` doesn't match ([#76](https://github.com/amannn/action-semantic-pull-request/issues/76)) ([e617d81](https://github.com/amannn/action-semantic-pull-request/commit/e617d811330c87d229d4d7c9a91517f6197869a2)) 273 | 274 | ## [3.1.0](https://github.com/amannn/action-semantic-pull-request/compare/v3.0.0...v3.1.0) (2021-01-11) 275 | 276 | 277 | ### Features 278 | 279 | * Add regex validation for subject ([#71](https://github.com/amannn/action-semantic-pull-request/issues/71)) ([04b071e](https://github.com/amannn/action-semantic-pull-request/commit/04b071e606842fe1c6b50fcbc0cab856c7d1cb49)) 280 | 281 | ## [3.0.0](https://github.com/amannn/action-semantic-pull-request/compare/v2.2.0...v3.0.0) (2021-01-08) 282 | 283 | 284 | ### ⚠ BREAKING CHANGES 285 | 286 | * Add opt-in for WIP (#73) 287 | 288 | ### Features 289 | 290 | * Add opt-in for WIP ([#73](https://github.com/amannn/action-semantic-pull-request/issues/73)) ([fb077fa](https://github.com/amannn/action-semantic-pull-request/commit/fb077fa571d6e14bc0ba9bc5b971e741ac693399)) 291 | 292 | ## [2.2.0](https://github.com/amannn/action-semantic-pull-request/compare/v2.1.1...v2.2.0) (2020-12-21) 293 | 294 | 295 | ### Features 296 | 297 | * Add ability to constrain scopes ([#66](https://github.com/amannn/action-semantic-pull-request/issues/66)) ([95b7031](https://github.com/amannn/action-semantic-pull-request/commit/95b703180f65c7da62280f216fb2a6fcc176dd26)) 298 | 299 | ### [2.1.1](https://github.com/amannn/action-semantic-pull-request/compare/v2.1.0...v2.1.1) (2020-12-03) 300 | 301 | 302 | ### Bug Fixes 303 | 304 | * Get rid of deprecation notice ([#63](https://github.com/amannn/action-semantic-pull-request/issues/63)) ([ec157ab](https://github.com/amannn/action-semantic-pull-request/commit/ec157abe0cb1f9c0ec79c022db8a5e6245f53df8)) 305 | 306 | ## [2.1.0](https://github.com/amannn/action-semantic-pull-request/compare/v2.0.0...v2.1.0) (2020-10-21) 307 | 308 | 309 | ### Features 310 | 311 | * Allow configuration for custom types ([@alorma](https://github.com/alorma), [@mrchief](https://github.com/mrchief) and [@amannn](https://github.com/amannn) in [#53](https://github.com/amannn/action-semantic-pull-request/issues/53)) ([2fe39e2](https://github.com/amannn/action-semantic-pull-request/commit/2fe39e2ce8ed0337bff045b6b6457e685d0dd51f)) 312 | 313 | ## [2.0.0](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.10...v2.0.0) (2020-09-17) 314 | 315 | 316 | ### ⚠ BREAKING CHANGES 317 | 318 | * Remove support for `improvement` prefix (as per commitizen/conventional-commit-types#16). 319 | 320 | ### Miscellaneous Chores 321 | 322 | * Update dependencies. ([b9bc3f1](https://github.com/amannn/action-semantic-pull-request/commit/b9bc3f12d1e30b03273a4077cb7936b091fb1ba2)) 323 | 324 | ### [1.2.10](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.9...v1.2.10) (2020-09-17) 325 | 326 | 327 | ### ⚠ BREAKING CHANGES 328 | 329 | * Remove support for `improvement` prefix (as per https://github.com/commitizen/conventional-commit-types/pull/16). 330 | 331 | ### Miscellaneous Chores 332 | 333 | * Update dependencies ([#31](https://github.com/amannn/action-semantic-pull-request/issues/31)) ([2aa2eb7](https://github.com/amannn/action-semantic-pull-request/commit/2aa2eb7e08ff8a6d0eaf3d42df0efec1cdeb1984)) 334 | 335 | ### [1.2.9](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.8...v1.2.9) (2020-09-17) 336 | 337 | 338 | ### Bug Fixes 339 | 340 | * Improve reporting for failed checks ([#30](https://github.com/amannn/action-semantic-pull-request/issues/30)) ([1aa9e17](https://github.com/amannn/action-semantic-pull-request/commit/1aa9e172840b7e07c0751e80aa03271b80d27ebe)) 341 | 342 | ### [1.2.8](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.7...v1.2.8) (2020-08-23) 343 | 344 | ### [1.2.7](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.6...v1.2.7) (2020-08-22) 345 | 346 | 347 | ### Bug Fixes 348 | 349 | * update workflow to use pull_request_target ([#25](https://github.com/amannn/action-semantic-pull-request/issues/25)) ([73f02a4](https://github.com/amannn/action-semantic-pull-request/commit/73f02a44b31b2c716caf45cc18e5e12d405ae225)), closes [#24](https://github.com/amannn/action-semantic-pull-request/issues/24) 350 | 351 | ### [1.2.6](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.5...v1.2.6) (2020-08-14) 352 | 353 | ### [1.2.5](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.4...v1.2.5) (2020-08-14) 354 | 355 | ### [1.2.4](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.3...v1.2.4) (2020-08-14) 356 | 357 | ### [1.2.3](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.2...v1.2.3) (2020-08-14) 358 | 359 | ### [1.2.2](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.1...v1.2.2) (2020-06-15) 360 | 361 | 362 | ### Bug Fixes 363 | 364 | * Bump npm from 6.13.0 to 6.14.5 ([#8](https://github.com/amannn/action-semantic-pull-request/issues/8)) ([9779e20](https://github.com/amannn/action-semantic-pull-request/commit/9779e20f1998f8b97180af283e8f4b29f120d44f)) 365 | 366 | ### [1.2.1](https://github.com/amannn/action-semantic-pull-request/compare/v1.2.0...v1.2.1) (2020-05-13) 367 | 368 | ## [1.2.0](https://github.com/amannn/action-semantic-pull-request/compare/v1.1.1...v1.2.0) (2019-11-20) 369 | 370 | 371 | ### Features 372 | 373 | * Add [WIP] support. ([#3](https://github.com/amannn/action-semantic-pull-request/issues/3)) ([2b4d25e](https://github.com/amannn/action-semantic-pull-request/commit/2b4d25ed4b55efc389f5e5898b061615ae96a1da)) 374 | 375 | ### 1.1.1 First usable release 376 | --------------------------------------------------------------------------------