├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── action.yml ├── commitlint.config.js ├── dist ├── commit.hbs ├── footer.hbs ├── header.hbs ├── index.js └── template.hbs ├── jest.config.js ├── package.json ├── renovate.json ├── src ├── linter.ts └── main.ts ├── test └── linter.test.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 16 | "@typescript-eslint/no-require-imports": "error", 17 | "@typescript-eslint/array-type": "error", 18 | "@typescript-eslint/await-thenable": "error", 19 | "@typescript-eslint/ban-ts-comment": "error", 20 | "camelcase": "off", 21 | "@typescript-eslint/consistent-type-assertions": "error", 22 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 23 | "@typescript-eslint/func-call-spacing": ["error", "never"], 24 | "@typescript-eslint/no-array-constructor": "error", 25 | "@typescript-eslint/no-empty-interface": "error", 26 | "@typescript-eslint/no-explicit-any": "error", 27 | "@typescript-eslint/no-extraneous-class": "error", 28 | "@typescript-eslint/no-for-in-array": "error", 29 | "@typescript-eslint/no-inferrable-types": "error", 30 | "@typescript-eslint/no-misused-new": "error", 31 | "@typescript-eslint/no-namespace": "error", 32 | "@typescript-eslint/no-non-null-assertion": "warn", 33 | "@typescript-eslint/no-unnecessary-qualifier": "error", 34 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 35 | "@typescript-eslint/no-useless-constructor": "error", 36 | "@typescript-eslint/no-var-requires": "error", 37 | "@typescript-eslint/prefer-for-of": "warn", 38 | "@typescript-eslint/prefer-function-type": "warn", 39 | "@typescript-eslint/prefer-includes": "error", 40 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 41 | "@typescript-eslint/promise-function-async": "error", 42 | "@typescript-eslint/require-array-sort-compare": "error", 43 | "@typescript-eslint/restrict-plus-operands": "error", 44 | "semi": "off", 45 | "@typescript-eslint/semi": ["error", "never"], 46 | "@typescript-eslint/type-annotation-spacing": "error", 47 | "@typescript-eslint/unbound-method": "error" 48 | }, 49 | "env": { 50 | "node": true, 51 | "es6": true, 52 | "jest/globals": true 53 | } 54 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "build" 2 | on: 3 | push: 4 | 5 | jobs: 6 | build: # make sure build/ci work properly 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - run: | 11 | yarn 12 | yarn all -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | on: 3 | pull_request: 4 | types: ['opened', 'edited', 'reopened', 'synchronize'] 5 | 6 | jobs: 7 | test: # make sure the action works on a clean machine without building 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - run: | 12 | yarn 13 | yarn build:pack 14 | - uses: ./ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | .idea/ 102 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️ Pull request name linter with commitlint ⚡️ 2 | --- 3 | [![build](https://github.com/JulienKode/pull-request-name-linter-action/workflows/build/badge.svg)](https://github.com/JulienKode/pull-request-name-linter-action/actions) 4 | [![GitHub issues](https://img.shields.io/github/issues/JulienKode/pull-request-name-linter-action?style=flat-square)](https://github.com/JulienKode/pull-request-name-linter-action/issues) 5 | [![GitHub forks](https://img.shields.io/github/forks/JulienKode/pull-request-name-linter-action?style=flat-square)](https://github.com/JulienKode/pull-request-name-linter-action/network) 6 | [![GitHub stars](https://img.shields.io/github/stars/JulienKode/pull-request-name-linter-action?style=flat-square)](https://github.com/JulienKode/pull-request-name-linter-action/stargazers) 7 | [![GitHub license](https://img.shields.io/github/license/JulienKode/pull-request-name-linter-action?style=flat-square)](https://github.com/JulienKode/pull-request-name-linter-action/blob/master/LICENSE) 8 | [![Watch on GitHub](https://img.shields.io/github/watchers/JulienKode/pull-request-name-linter-action.svg?style=social)](https://github.com/JulienKode/pull-request-name-linter-action/watchers) 9 | [![Tweet](https://img.shields.io/twitter/url/https/github.com/JulienKode/pull-request-name-linter-action.svg?style=social)](https://twitter.com/intent/tweet?text=Checkout%20this%20library%20https%3A%2F%2Fgithub.com%2FJulienKode%2Fpull-request-name-linter-action) 10 | --- 11 | 12 | **GitHub action** to automatically **lint pull request name** with [**commitlint**](https://commitlint.js.org). 13 | This is useful if squash merge your pull request for example. 14 | 15 | **Note**: If you are looking to lint the commits of your pull request with commitlint you can use [commitlint-github-action](https://github.com/wagoid/commitlint-github-action) 16 | 17 | This package are using the commitlint 11 version 18 | 19 | ## Configuration 20 | 21 | ## Usage 22 | 23 | ### Create `.github/workflows/pr-name.yml` 24 | 25 | Create a workflow (eg: `.github/workflows/pr-name.yml` see [Creating a Workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file)). 26 | Here is an example of configuration 27 | 28 | ```yaml 29 | name: pr-name-linter 30 | on: 31 | pull_request: 32 | types: ['opened', 'edited', 'reopened', 'synchronize'] 33 | 34 | jobs: 35 | lint: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v1 39 | - name: Install Dependencies 40 | run: npm install @commitlint/config-conventional 41 | - uses: JulienKode/pull-request-name-linter-action@v0.5.0 42 | ``` 43 | 44 | **Note**: make sure you install your dependencies that commitlint use 45 | 46 | ## Example 47 | 48 | If you want to see an example of usage you can checkout this repository: https://github.com/JulienKode/pull-request-name-linter-action-example 49 | 50 | ![image](https://user-images.githubusercontent.com/7658664/80835181-8a7cc280-8bf2-11ea-932b-7a954db6bf60.png) 51 | 52 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Pull request linter action' 2 | description: "Lint your pull request name based on commitlint configuration" 3 | author: 'Julien Karst' 4 | inputs: 5 | configuration-path: 6 | description: 'The path for the commitlint configurations' 7 | required: false 8 | default: './commitlint.config.js' 9 | runs: 10 | using: 'node12' 11 | main: 'dist/index.js' 12 | branding: 13 | icon: 'users' 14 | color: 'blue' 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { rules: { "scope-case": [2, "always", "lower-case"] } }; 2 | -------------------------------------------------------------------------------- /dist/commit.hbs: -------------------------------------------------------------------------------- 1 | *{{#if scope}} **{{scope}}:** 2 | {{~/if}} {{#if subject}} 3 | {{~subject}} 4 | {{~else}} 5 | {{~header}} 6 | {{~/if}} 7 | 8 | {{~!-- commit link --}} {{#if @root.linkReferences~}} 9 | ([{{hash}}]( 10 | {{~#if @root.repository}} 11 | {{~#if @root.host}} 12 | {{~@root.host}}/ 13 | {{~/if}} 14 | {{~#if @root.owner}} 15 | {{~@root.owner}}/ 16 | {{~/if}} 17 | {{~@root.repository}} 18 | {{~else}} 19 | {{~@root.repoUrl}} 20 | {{~/if}}/ 21 | {{~@root.commit}}/{{hash}})) 22 | {{~else}} 23 | {{~hash}} 24 | {{~/if}} 25 | 26 | {{~!-- commit references --}} 27 | {{~#if references~}} 28 | , closes 29 | {{~#each references}} {{#if @root.linkReferences~}} 30 | [ 31 | {{~#if this.owner}} 32 | {{~this.owner}}/ 33 | {{~/if}} 34 | {{~this.repository}}#{{this.issue}}]( 35 | {{~#if @root.repository}} 36 | {{~#if @root.host}} 37 | {{~@root.host}}/ 38 | {{~/if}} 39 | {{~#if this.repository}} 40 | {{~#if this.owner}} 41 | {{~this.owner}}/ 42 | {{~/if}} 43 | {{~this.repository}} 44 | {{~else}} 45 | {{~#if @root.owner}} 46 | {{~@root.owner}}/ 47 | {{~/if}} 48 | {{~@root.repository}} 49 | {{~/if}} 50 | {{~else}} 51 | {{~@root.repoUrl}} 52 | {{~/if}}/ 53 | {{~@root.issue}}/{{this.issue}}) 54 | {{~else}} 55 | {{~#if this.owner}} 56 | {{~this.owner}}/ 57 | {{~/if}} 58 | {{~this.repository}}#{{this.issue}} 59 | {{~/if}}{{/each}} 60 | {{~/if}} 61 | 62 | -------------------------------------------------------------------------------- /dist/footer.hbs: -------------------------------------------------------------------------------- 1 | {{#if noteGroups}} 2 | {{#each noteGroups}} 3 | 4 | ### {{title}} 5 | 6 | {{#each notes}} 7 | * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}} 8 | {{/each}} 9 | {{/each}} 10 | 11 | {{/if}} 12 | -------------------------------------------------------------------------------- /dist/header.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if isPatch~}} 3 | ## 4 | {{~else~}} 5 | # 6 | {{~/if}} {{#if @root.linkCompare~}} 7 | [{{version}}]( 8 | {{~#if @root.repository~}} 9 | {{~#if @root.host}} 10 | {{~@root.host}}/ 11 | {{~/if}} 12 | {{~#if @root.owner}} 13 | {{~@root.owner}}/ 14 | {{~/if}} 15 | {{~@root.repository}} 16 | {{~else}} 17 | {{~@root.repoUrl}} 18 | {{~/if~}} 19 | /compare/{{previousTag}}...{{currentTag}}) 20 | {{~else}} 21 | {{~version}} 22 | {{~/if}} 23 | {{~#if title}} "{{title}}" 24 | {{~/if}} 25 | {{~#if date}} ({{date}}) 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /dist/template.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | 3 | {{#each commitGroups}} 4 | 5 | {{#if title}} 6 | ### {{title}} 7 | 8 | {{/if}} 9 | {{#each commits}} 10 | {{> commit root=@root}} 11 | {{/each}} 12 | 13 | {{/each}} 14 | {{> footer}} 15 | 16 | 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pull-request-commitlint-action", 3 | "version": "0.1.2", 4 | "private": true, 5 | "description": "Run commitlint on pull request name.", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "pack": "ncc build", 13 | "test": "jest", 14 | "build:pack": "npm run build && npm run pack", 15 | "all": "npm run build && npm run test && npm run format && npm run lint && npm run pack" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/actions/typescript-action.git" 20 | }, 21 | "keywords": [ 22 | "actions", 23 | "node", 24 | "setup" 25 | ], 26 | "author": "Julien Karst", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@commitlint/types": "17.8.1", 30 | "@types/jest": "27.5.2", 31 | "@types/node": "16.11.45", 32 | "@typescript-eslint/parser": "5.62.0", 33 | "@vercel/ncc": "0.38.1", 34 | "eslint": "7.32.0", 35 | "eslint-plugin-github": "3.4.1", 36 | "eslint-plugin-jest": "26.9.0", 37 | "eslint-plugin-prettier": "4.2.1", 38 | "jest": "27.5.1", 39 | "jest-circus": "27.5.1", 40 | "js-yaml": "4.1.0", 41 | "prettier": "2.8.8", 42 | "ts-jest": "27.1.5", 43 | "typescript": "3.9.10" 44 | }, 45 | "dependencies": { 46 | "@actions/core": "1.9.1", 47 | "@actions/github": "5.1.1", 48 | "@commitlint/lint": "17.8.1", 49 | "@commitlint/load": "17.8.1" 50 | }, 51 | "resolutions": { 52 | "import-fresh": "JulienKode/import-fresh#fix/issue-when-there-no-parent-module" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "automerge": true, 3 | "extends": [ 4 | "config:base", 5 | "schedule:monthly" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/linter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LintOptions, 3 | ParserOptions, 4 | ParserPreset, 5 | QualifiedConfig 6 | } from '@commitlint/types' 7 | import load from '@commitlint/load' 8 | import lint from '@commitlint/lint' 9 | 10 | function selectParserOpts(parserPreset: ParserPreset): object | null { 11 | if (typeof parserPreset !== 'object') { 12 | return null 13 | } 14 | 15 | if (typeof parserPreset.parserOpts !== 'object') { 16 | return null 17 | } 18 | 19 | return parserPreset.parserOpts 20 | } 21 | 22 | function getLintOptions(configuration: QualifiedConfig): LintOptions { 23 | const opts: LintOptions & {parserOpts: ParserOptions} = { 24 | parserOpts: {}, 25 | plugins: {}, 26 | ignores: [], 27 | defaultIgnores: true 28 | } 29 | if (configuration.parserPreset) { 30 | const parserOpts = selectParserOpts(configuration.parserPreset) 31 | if (parserOpts) opts.parserOpts = parserOpts 32 | } 33 | if (configuration.plugins) { 34 | opts.plugins = configuration.plugins 35 | } 36 | if (configuration.ignores) { 37 | opts.ignores = configuration.ignores 38 | } 39 | if (!configuration.defaultIgnores) { 40 | opts.defaultIgnores = false 41 | } 42 | return opts 43 | } 44 | 45 | export async function lintPullRequest( 46 | title: string, 47 | configPath: string 48 | ): Promise { 49 | const configuration = await load({}, {file: configPath, cwd: process.cwd()}) 50 | 51 | const options = getLintOptions(configuration) 52 | 53 | const result = await lint(title, configuration.rules, options) 54 | 55 | if (result.valid) return 56 | const errorMessage = result.errors 57 | .map(({message, name}) => `${name}:${message}`) 58 | .join('\n') 59 | throw new Error(errorMessage) 60 | } 61 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import {lintPullRequest} from './linter' 4 | 5 | async function run(): Promise { 6 | try { 7 | const configPath = core.getInput('configuration-path', {required: true}) 8 | const title = getPrTitle() 9 | if (!title) { 10 | core.debug('Could not get pull request title from context, exiting') 11 | return 12 | } 13 | await lintPullRequest(title, configPath) 14 | } catch (error) { 15 | core.error(error) 16 | core.setFailed(error.message) 17 | } 18 | } 19 | 20 | function getPrTitle(): string | undefined { 21 | const pullRequest = github.context.payload.pull_request 22 | if (!pullRequest) { 23 | return undefined 24 | } 25 | 26 | return pullRequest.title 27 | } 28 | 29 | run() 30 | -------------------------------------------------------------------------------- /test/linter.test.ts: -------------------------------------------------------------------------------- 1 | import {lintPullRequest} from '../src/linter' 2 | 3 | test('should lint the title correctly', async () => { 4 | // Given 5 | const title = 'feat(valid): scope' 6 | 7 | // Expect 8 | await expect( 9 | lintPullRequest(title, './commitlint.config.js') 10 | ).resolves.toBeUndefined() 11 | }) 12 | 13 | test('should raise errors', async () => { 14 | // Given 15 | const title = 'feat(INVALID): scope' 16 | 17 | // Expect 18 | await expect( 19 | lintPullRequest(title, './commitlint.config.js') 20 | ).rejects.toThrow() 21 | }) 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts"] 12 | } 13 | --------------------------------------------------------------------------------