Skip to content

Commit 433c10a

Browse files
Initial Commit
Commit the core logic of message format check
1 parent 74d82b2 commit 433c10a

13 files changed

+8524
-2
lines changed

.gitignore

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
### Code ###
2+
.vscode/*
3+
!.vscode/settings.json
4+
!.vscode/tasks.json
5+
!.vscode/launch.json
6+
!.vscode/extensions.json
7+
8+
### Node ###
19
# Logs
210
logs
311
*.log
412
npm-debug.log*
513
yarn-debug.log*
614
yarn-error.log*
15+
lerna-debug.log*
16+
17+
# Diagnostic reports (https://nodejs.org/api/report.html)
18+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
719

820
# Runtime data
921
pids
@@ -20,7 +32,7 @@ coverage
2032
# nyc test coverage
2133
.nyc_output
2234

23-
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
35+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
2436
.grunt
2537

2638
# Bower dependency directory (https://bower.io/)
@@ -56,6 +68,29 @@ typings/
5668

5769
# dotenv environment variables file
5870
.env
71+
.env.test
72+
73+
# parcel-bundler cache (https://parceljs.org/)
74+
.cache
5975

6076
# next.js build output
6177
.next
78+
79+
# nuxt.js build output
80+
.nuxt
81+
82+
# vuepress build output
83+
.vuepress/dist
84+
85+
# Serverless directories
86+
.serverless/
87+
88+
# FuseBox cache
89+
.fusebox/
90+
91+
# DynamoDB Local files
92+
.dynamodb/
93+
94+
### Serverless ###
95+
# Ignore build directory
96+
.serverless

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,31 @@
11
# commit-message-validator
2-
Github app to validate commit message on a pull request
2+
Github app to validate commit message and pull request title on a pull request
3+
4+
## Description
5+
This app runs a format check on commit messages and pull request title on the creation of a pull request.
6+
7+
For example, let's say you specify that a commit message should have a format `DDD:message`. Here D stand for numeric digit. The app checks if the commit message follows this format. If all the commit messages follow this format, the check returns successful, otherwise failure. The reviewer can then decide if they want to go ahead with the code merge.
8+
9+
### App URL
10+
https://github.com/apps/commit-message-validator
11+
12+
## Installation
13+
14+
Use the Github's app section or above URL to install the app to your repository.
15+
16+
## Setup
17+
18+
You would need to add a configuration file named `.validationconfig` to the root of your repository. The contents of that file will be:
19+
20+
```
21+
PR_TITLE_REGEX=<PR Title Regex>
22+
COMMIT_MESSAGE_REGEX=<Commit Message Regex>
23+
```
24+
25+
## Usage
26+
Go to the `checks` section on your PR to see the result of the check run performed by the app. It will show you the result as well as the commit messages which failed.
27+
28+
## Contributors
29+
[Anshul Soni](https://www.linkedin.com/in/anshul-soni-3903a2101/)
30+
31+
[Sumit Singhal](https://www.linkedin.com/in/s-singhal)

controllers/pullRequest.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
const { checkRegex } = require('../utilities/regexUtil');
2+
const { getRegexFromConfig } = require('../helpers/config');
3+
const { createCheckSuite, createCheckRun, listCheckSuite } = require('../helpers/checks');
4+
const { listCommitsOfPullRequest } = require('../helpers/pullRequest');
5+
const defaultJson = require('../default.json');
6+
const conclusionStatus = defaultJson.conclusion_status;
7+
const messages = defaultJson.messages;
8+
const checkRunStatusCompleted = defaultJson.CHECK_RUN_STATUS_COMPLETED;
9+
const checkRunName = defaultJson.CHECK_RUN_NAME;
10+
const outputTitleSuccess = defaultJson.output_title_success;
11+
const outputTitleFail = defaultJson.output_title_fail;
12+
13+
module.exports.commitAndTitleValidator = async (app, context) => {
14+
try {
15+
let owner = context.payload.repository.owner.login;
16+
let repository = context.payload.repository.name;
17+
let pullRequestTitle = context.payload.pull_request.title;
18+
let pullNumber = context.payload.number;
19+
let regexData = await getRegexFromConfig(app, owner, repository, process.env.REGEX_CONFIG_FILE_NAME);
20+
// find commits
21+
let commits = await listCommitsOfPullRequest(context, owner, repository, pullNumber);
22+
let result = checkMessagesFormat(pullRequestTitle, commits.data, regexData.prTitle, regexData.commitMsg);
23+
if (result && result.commitIds && Array.isArray(result.commitIds) && result.commitIds.length) {
24+
for (let index = 0; index < result.commitIds.length; index++) {
25+
const commitId = result.commitIds[index];
26+
/**
27+
* check if checkSuite exists or not for the commit
28+
*/
29+
let checkSuiteList = await listCheckSuite(context, owner, repository, commitId);
30+
if (!checkSuiteList || (checkSuiteList && checkSuiteList.data && checkSuiteList.data.total_count && checkSuiteList.data.total_count === 0)) {
31+
// create check suite for a particular commit
32+
await createCheckSuite(context, owner, repository, commitId);
33+
}
34+
// create check run
35+
await createCheckRun(context, owner, repository, commitId, result.status, result.checkRunName, result.conclusion, result.output);
36+
}
37+
}
38+
} catch (error) {
39+
app.log(error);
40+
}
41+
};
42+
43+
/**
44+
* check Messages Format
45+
* @param {String} pullRequestTitle
46+
* @param {Array} commits
47+
* @param {Object} prTitle
48+
* @param {Object} commitMsg
49+
*/
50+
function checkMessagesFormat(pullRequestTitle, commits, prTitle, commitMsg) {
51+
try {
52+
let result = {};
53+
let commitIds = [];
54+
let invalidCommits = '';
55+
let invalidCommitsCount = 0;
56+
let otherInvalidCommitMessages = '';
57+
let conclusion = conclusionStatus.FAILURE;
58+
let pullReqTitleStatus = false;
59+
let pullReqTitleStatusMsg = '';
60+
let commitMsgStatus = true;
61+
let commitMsgStatusMsg = messages.valid_commit_message;
62+
let outputTitle = outputTitleFail;
63+
/**
64+
* pull Request Title check : starts
65+
*/
66+
// pull request title format
67+
let mergePattern = /^(Merge pull request)/;
68+
if (checkRegex(pullRequestTitle, prTitle)) {
69+
pullReqTitleStatus = true;
70+
pullReqTitleStatusMsg = messages.valid_pull_request_message;
71+
} else {
72+
pullReqTitleStatus = false;
73+
pullReqTitleStatusMsg = messages.invalid_pull_request_message;
74+
// invalid pull Request title
75+
}
76+
/**
77+
* pull Request Title check : ends
78+
*/
79+
/**
80+
* commit message check : starts
81+
*/
82+
// find commits
83+
if (commits && Array.isArray(commits) && commits.length) {
84+
// check all commit messages
85+
for (let index = 0; index < commits.length; index++) {
86+
const element = commits[index];
87+
const commitMessage = element.commit.message;
88+
commitIds.push(commits[index].sha);
89+
if (!checkRegex(commitMessage, commitMsg) && !checkRegex(commitMessage, mergePattern)) {
90+
invalidCommitsCount++;
91+
commitMsgStatus = false;
92+
commitMsgStatusMsg = messages.invalid_commit_message;
93+
if (invalidCommitsCount <= defaultJson.INVALID_COMMIT_LIMIT) {
94+
invalidCommits += `${defaultJson.invalid_commit_list.commit_id} ${commits[index].sha} | ${defaultJson.invalid_commit_list.commit_message} ${commitMessage} <br/>`;
95+
if (invalidCommitsCount === 1) {
96+
otherInvalidCommitMessages = messages.single_other_invalid_message;
97+
} else {
98+
otherInvalidCommitMessages = messages.multiple_other_invalid_message;
99+
}
100+
}
101+
}
102+
}
103+
/**
104+
* commit message check : ends
105+
*/
106+
/**
107+
* check if both the messages are valid for regex
108+
*/
109+
if (pullReqTitleStatus && commitMsgStatus) {
110+
conclusion = conclusionStatus.SUCCESS;
111+
outputTitle = outputTitleSuccess;
112+
}
113+
114+
/**
115+
* set check run status
116+
*/
117+
let status = checkRunStatusCompleted;
118+
let output = {
119+
title: outputTitle,
120+
summary: `${pullReqTitleStatusMsg}<br/>${commitMsgStatusMsg}<br/>${invalidCommits}<br/>${invalidCommitsCount} ${otherInvalidCommitMessages}`
121+
};
122+
result = {
123+
commitIds,
124+
status,
125+
checkRunName,
126+
conclusion,
127+
output
128+
};
129+
}
130+
return result;
131+
} catch (error) {
132+
console.error(error);
133+
}
134+
}

default.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"conclusion_status": {
3+
"SUCCESS": "success",
4+
"FAILURE": "failure",
5+
"NEUTRAL": "neutral",
6+
"CANCELLED": "cancelled",
7+
"TIMED_OUT": "timed_out",
8+
"ACTION_REQUIRED": "action_required"
9+
},
10+
"messages": {
11+
"valid_commit_message": "Commit messages are valid",
12+
"invalid_commit_message": "Commit messages are invalid",
13+
"valid_pull_request_message": "Pull request title is valid",
14+
"invalid_pull_request_message": "Pull request title is invalid",
15+
"multiple_other_invalid_message": "other messages are invalid",
16+
"single_other_invalid_message": "other message is invalid"
17+
},
18+
"CHECK_RUN_STATUS_COMPLETED": "completed",
19+
"CHECK_RUN_NAME": "commit message validator",
20+
"output_title_success": "Message validation passed!!!",
21+
"output_title_fail": "Message validation failed!!!",
22+
"output_summary": "",
23+
"INVALID_COMMIT_LIMIT": 3,
24+
"USER_AGENT": "commit-message-validator-app",
25+
"CONFIG_FILENAME": "validationconfig",
26+
"invalid_commit_list": {
27+
"commit_id": "sha:",
28+
"commit_message": "message:"
29+
},
30+
"REGEX": {
31+
"PR_TITLE_REGEX": "PR_TITLE_REGEX=",
32+
"COMMIT_MESSAGE_REGEX": "COMMIT_MESSAGE_REGEX="
33+
}
34+
}

helpers/checks.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* create Check Suite
3+
* @param {Object} context The title of the pull request.
4+
* @param {String} owner
5+
* @param {String} repo
6+
* @param {String} commitId
7+
*/
8+
module.exports.createCheckSuite = async (context, owner, repo, commitId) => {
9+
try {
10+
let params = {
11+
owner: owner,
12+
13+
repo: repo,
14+
/**
15+
* The sha of the head commit.
16+
*/
17+
head_sha: commitId,
18+
};
19+
let checkSuite = await context.github.checks.createSuite(params);
20+
return checkSuite;
21+
} catch (error) {
22+
return error;
23+
}
24+
};
25+
26+
/**
27+
* create CheckRun for a commit
28+
* @param {Object} context The title of the pull request.
29+
* @param {String} owner
30+
* @param {String} repo
31+
* @param {String} commitId
32+
* @param {String} status
33+
* @param {String} checkRunName
34+
* @param {String} conclusion
35+
*/
36+
module.exports.createCheckRun = async (context, owner, repo, commitId, status, checkRunName, conclusion, output) => {
37+
try {
38+
let params = {
39+
owner: owner,
40+
repo: repo,
41+
/**
42+
* The name of the check. For example, "code-coverage".
43+
*/
44+
name: checkRunName,
45+
/**
46+
* The SHA of the commit.
47+
*/
48+
head_sha: commitId,
49+
/**
50+
* The current status. Can be one of `queued`, `in_progress`, or `completed`.
51+
*/
52+
status: status,
53+
/**
54+
* The time that the check run began in ISO 8601 format: `YYYY-MM-DDTHH:MM:SSZ`.
55+
*/
56+
started_at: new Date().toISOString(),
57+
conclusion: conclusion,
58+
completed_at: new Date().toISOString(),
59+
output: output
60+
};
61+
let checkRun = await context.github.checks.create(params);
62+
return checkRun;
63+
} catch (error) {
64+
return error;
65+
}
66+
};
67+
68+
/**
69+
* list Check Suite for a commit
70+
* @param {Object} context The title of the pull request.
71+
* @param {String} owner
72+
* @param {String} repo
73+
* @param {String} ref
74+
*/
75+
module.exports.listCheckSuite = async (context, owner, repo, ref) => {
76+
try {
77+
let params = {
78+
owner: owner,
79+
repo: repo,
80+
ref: ref
81+
};
82+
let checkRun = await context.github.checks.listSuitesForRef(params);
83+
return checkRun;
84+
} catch (error) {
85+
return error;
86+
}
87+
};

0 commit comments

Comments
 (0)