├── docs └── screenshot.png ├── jest.config.js ├── index.js ├── .prettierrc.js ├── tsconfig.json ├── babel.config.js ├── .gitignore ├── action.yml ├── CONTRIBUTING.md ├── package.json ├── README.md └── src ├── __tests__ └── isValidCommitMessage.test.ts ├── extractCommits.ts ├── main.ts └── isValidCommitMesage.ts /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webiny/action-conventional-commits/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | testEnvironment: "node", 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const got = require("got"); 2 | 3 | got.get("https://api.github.com/repos/doitadrian/contreebutors-action/pulls/2/commits", { 4 | responseType: "json", 5 | }).then((response) => { 6 | console.log(response.body); 7 | }); 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | overrides: [ 5 | { 6 | files: ["*.js", "*.ts", "*.tsx"], 7 | options: { 8 | tabWidth: 4 9 | } 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*" 4 | ], 5 | "exclude": [ 6 | "node_modules", 7 | "./node_modules", 8 | "./node_modules/*" 9 | ], 10 | "compilerOptions": { 11 | "target": "es2016", 12 | "moduleResolution": "node", 13 | "types": [] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript', 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .webpack 3 | .webiny 4 | .serverless 5 | .env* 6 | .npmrc 7 | htpasswd 8 | lerna-debug.log 9 | npm-debug.log 10 | node_modules 11 | .DS_Store 12 | coverage 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lib/ 17 | build/ 18 | .files/ 19 | .changelog 20 | .verdaccio 21 | .history 22 | *.tsbuildinfo 23 | .cloudfnsrc.js 24 | .cloudfns/ 25 | yarn.lock 26 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Conventional Commits Action" 2 | description: "Ensures that all commit messages are following the conventional-commits standard." 3 | inputs: 4 | GITHUB_TOKEN: 5 | description: 'GitHub token' 6 | required: false 7 | allowed-commit-types: 8 | description: 'Specify a comma separated list of allowed commit types' 9 | default: 'feat,fix,docs,style,refactor,test,build,perf,ci,chore,revert,merge,wip' 10 | required: false 11 | 12 | runs: 13 | using: node20 14 | main: dist/main/index.js 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | 1. Fork and clone the repository locally. 4 | 2. Run `yarn` to install all of the dependencies. 5 | 3. Start developing with the `yarn watch` command. 6 | 4. Tests can be run with the `yarn test` command. 7 | 5. Build code with the `yarn build` command. 8 | 9 | ## Releasing a New Version 10 | 11 | First, build all the code via `yarn build`. 12 | 13 | Secondly, commit all of the changes you have locally (even the changes in `dist` folder) and then use the following commands to create a tag and push / release everything: 14 | 15 | ``` 16 | git tag -a -m "Release v1.0.19" v1.0.19 17 | git push --follow-tags 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-conventional-commits", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:doitadrian/action-conventional-commits.git", 6 | "author": "Adrian Smijulj ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@actions/core": "^1.2.3", 10 | "@actions/exec": "^1.0.3", 11 | "@actions/github": "^2.1.1", 12 | "got": "^11.3.0", 13 | "lodash.get": "^4.4.2" 14 | }, 15 | "devDependencies": { 16 | "@vercel/ncc": "^0.38.1", 17 | "@babel/core": "^7.10.2", 18 | "@babel/preset-env": "^7.10.2", 19 | "@babel/preset-typescript": "^7.10.1", 20 | "@types/jest": "^26.0.0", 21 | "babel-jest": "^26.0.1", 22 | "jest": "^26.0.1", 23 | "prettier": "^2.0.2", 24 | "typescript": "^5.3.2" 25 | }, 26 | "scripts": { 27 | "build": "ncc build src/main.ts --out dist/main", 28 | "watch": "ncc build src/main.ts --out dist/main --watch", 29 | "test": "jest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conventional Commits GitHub Action 2 | 3 | A simple GitHub action that makes sure all commit messages are following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) specification. 4 | 5 | ![Screenshot](/docs/screenshot.png) 6 | 7 | Note that, typically, you would make this check on a pre-commit hook (for example, using something like [Commitlint](https://commitlint.js.org/)), but those can easily be skipped, hence this GitHub action. 8 | 9 | ### Usage 10 | Latest version: `v1.3.0` 11 | 12 | ```yml 13 | name: Conventional Commits 14 | 15 | on: 16 | pull_request: 17 | branches: [ master ] 18 | 19 | jobs: 20 | build: 21 | name: Conventional Commits 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - uses: webiny/action-conventional-commits@v1.3.0 27 | with: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Optional, for private repositories. 29 | allowed-commit-types: "feat,fix" # Optional, set if you want a subset of commit types to be allowed. 30 | ``` 31 | -------------------------------------------------------------------------------- /src/__tests__/isValidCommitMessage.test.ts: -------------------------------------------------------------------------------- 1 | import isValidCommitMessage from "../isValidCommitMesage"; 2 | 3 | test("should be able to correctly validate the commit message", () => { 4 | expect(isValidCommitMessage("chore(nice-one): doing this right")).toBe(true); 5 | expect(isValidCommitMessage("feat!: change all the things")).toBe(true); 6 | expect(isValidCommitMessage("fix(user)!: a fix with some breaking changes")).toBe(true); 7 | expect(isValidCommitMessage("fix: menu must open on shortcut press")).toBe(true); 8 | expect(isValidCommitMessage("something: should not work")).toBe(false); 9 | expect(isValidCommitMessage("fixes something")).toBe(false); 10 | expect(isValidCommitMessage("🚧 fix: menu must open on shortcut press")).toBe(true); 11 | expect(isValidCommitMessage("fix(menus): menu must open on shortcut press")).toBe(true); 12 | expect(isValidCommitMessage("🚧 fix(menus): menu must open on shortcut press")).toBe(true); 13 | expect(isValidCommitMessage("🚧 fixing something")).toBe(false); 14 | expect(isValidCommitMessage("🚧 something: should not work")).toBe(false); 15 | expect(isValidCommitMessage("chorz: 123")).toBe(false); 16 | }); 17 | -------------------------------------------------------------------------------- /src/extractCommits.ts: -------------------------------------------------------------------------------- 1 | import get from "lodash.get"; 2 | import got from "got"; 3 | 4 | type Commit = { 5 | message: string; 6 | }; 7 | 8 | const extractCommits = async (context, core): Promise => { 9 | // For "push" events, commits can be found in the "context.payload.commits". 10 | const pushCommits = Array.isArray(get(context, "payload.commits")); 11 | if (pushCommits) { 12 | return context.payload.commits; 13 | } 14 | 15 | // For PRs, we need to get a list of commits via the GH API: 16 | const prCommitsUrl = get(context, "payload.pull_request.commits_url"); 17 | if (prCommitsUrl) { 18 | try { 19 | let requestHeaders = { 20 | "Accept": "application/vnd.github+json", 21 | } 22 | if (core.getInput('GITHUB_TOKEN') != "") { 23 | requestHeaders["Authorization"] = "token " + core.getInput('GITHUB_TOKEN') 24 | } 25 | const { body } = await got.get(prCommitsUrl, { 26 | responseType: "json", 27 | headers: requestHeaders, 28 | }); 29 | 30 | if (Array.isArray(body)) { 31 | return body.map((item) => item.commit); 32 | } 33 | return []; 34 | } catch { 35 | return []; 36 | } 37 | } 38 | 39 | return []; 40 | }; 41 | 42 | export default extractCommits; 43 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | const { context } = require("@actions/github"); 2 | const core = require("@actions/core"); 3 | 4 | import isValidCommitMessage from "./isValidCommitMesage"; 5 | import extractCommits from "./extractCommits"; 6 | 7 | async function run() { 8 | core.info( 9 | `ℹ️ Checking if commit messages are following the Conventional Commits specification...` 10 | ); 11 | 12 | const extractedCommits = await extractCommits(context, core); 13 | if (extractedCommits.length === 0) { 14 | core.info(`No commits to check, skipping...`); 15 | return; 16 | } 17 | 18 | let hasErrors; 19 | core.startGroup("Commit messages:"); 20 | for (let i = 0; i < extractedCommits.length; i++) { 21 | let commit = extractedCommits[i]; 22 | 23 | const allowedCommitTypes = core.getInput("allowed-commit-types").split(","); 24 | 25 | if (isValidCommitMessage(commit.message, allowedCommitTypes)) { 26 | core.info(`✅ ${commit.message}`); 27 | } else { 28 | core.info(`🚩 ${commit.message}`); 29 | hasErrors = true; 30 | } 31 | } 32 | core.endGroup(); 33 | 34 | if (hasErrors) { 35 | core.setFailed( 36 | `🚫 According to the conventional-commits specification, some of the commit messages are not valid.` 37 | ); 38 | } else { 39 | core.info("🎉 All commit messages are following the Conventional Commits specification."); 40 | } 41 | } 42 | 43 | run(); 44 | -------------------------------------------------------------------------------- /src/isValidCommitMesage.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_COMMIT_TYPES = [ 2 | "feat", 3 | "fix", 4 | "docs", 5 | "style", 6 | "refactor", 7 | "test", 8 | "build", 9 | "perf", 10 | "ci", 11 | "chore", 12 | "revert", 13 | "merge", 14 | "wip", 15 | ]; 16 | 17 | const isValidCommitMessage = (message: string, availableTypes = DEFAULT_COMMIT_TYPES): boolean => { 18 | // Exceptions. 19 | // This is a message that's auto-generated by git. Can't do much about it unfortunately. Let's allow it. 20 | if (message.startsWith("Merge ") || message.startsWith("Revert ")) { 21 | return true; 22 | } 23 | 24 | // Commit message doesn't fall into the exceptions group. Let's do the validation. 25 | let [possiblyValidCommitType] = message.split(":"); 26 | possiblyValidCommitType = possiblyValidCommitType.toLowerCase(); 27 | 28 | // Let's remove scope if present. 29 | if (possiblyValidCommitType.match(/\(\S*?\)/)) { 30 | possiblyValidCommitType = possiblyValidCommitType.replace(/\(\S*?\)/, ""); 31 | } 32 | 33 | possiblyValidCommitType = possiblyValidCommitType 34 | .replace(/\s/g, "") // Remove all whitespace 35 | .replace(/\!/g, "") // Remove bang for notify breaking change 36 | .replace(/()/g, "") // Remove all whitespace 37 | .replace(/[^a-z]/g, ""); // Only leave [a-z] characters. 38 | 39 | return availableTypes.includes(possiblyValidCommitType); 40 | }; 41 | 42 | export default isValidCommitMessage; 43 | --------------------------------------------------------------------------------