├── .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 | [](https://github.com/JulienKode/pull-request-name-linter-action/actions)
4 | [](https://github.com/JulienKode/pull-request-name-linter-action/issues)
5 | [](https://github.com/JulienKode/pull-request-name-linter-action/network)
6 | [](https://github.com/JulienKode/pull-request-name-linter-action/stargazers)
7 | [](https://github.com/JulienKode/pull-request-name-linter-action/blob/master/LICENSE)
8 | [](https://github.com/JulienKode/pull-request-name-linter-action/watchers)
9 | [](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 | 
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 |
--------------------------------------------------------------------------------