From 3c6f3e6d46947772a2bad0fa809b5b420d48ea1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nejdet=20Kadir=20Bekta=C5=9F?= <50639655+nejdetkadir@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:10:14 +0300 Subject: [PATCH] feat: add rule for branch name (#20) * feat: add rule for branch name * refactor: naming --- .github/workflows/ci.yml | 1 + README.md | 1 + action.yml | 4 ++ dist/index.js | 73 +++++++++++++++++++++++++++++++++++- src/constants/index.ts | 3 +- src/constants/inputKeys.ts | 1 + src/constants/pullRequest.ts | 17 +++++++++ src/hooks/useInputs.ts | 6 +++ src/lib/bot.ts | 22 +++++++++++ src/lib/pullRequest.ts | 31 ++++++++++++++- 10 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/constants/pullRequest.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 718983e..1c6bbd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,4 +69,5 @@ jobs: assigneeRequired: true checklistRequired: true semanticTitleRequired: true + semanticBranchNameRequired: true repoToken: ${{ secrets.SECRET_TOKEN }} diff --git a/README.md b/README.md index 2171d83..3c07e54 100644 --- a/README.md +++ b/README.md @@ -62,5 +62,6 @@ jobs: assigneeRequired: true # If true, the PR must have at least one assignee checklistRequired: true # If true, the PR must have a checklist on the PR body semanticTitleRequired: true # If true, the PR must have a semantic title. The title must follow the conventional commits specification + semanticBranchNameRequired: true # If true, the PR must have a semantic branch name repoToken: ${{ secrets.SECRET_TOKEN }} # Personal Access Token with repo scope ``` diff --git a/action.yml b/action.yml index a755b0d..9c57a69 100644 --- a/action.yml +++ b/action.yml @@ -19,6 +19,10 @@ inputs: description: 'Whether a semantic title is required or not' required: true default: 'true' + semanticBranchNameRequired: + description: 'Whether a semantic branch name is required or not' + required: true + default: 'true' repoToken: description: 'The repository token' required: true diff --git a/dist/index.js b/dist/index.js index caad11e..d357e74 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9679,7 +9679,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.QUESTIONS = exports.INPUT_KEYS = exports.COMMIT_KEYS = exports.CHECKLIST_KEYS = void 0; +exports.PULL_REQUEST = exports.QUESTIONS = exports.INPUT_KEYS = exports.COMMIT_KEYS = exports.CHECKLIST_KEYS = void 0; const checklistKeys_1 = __importDefault(__nccwpck_require__(6068)); exports.CHECKLIST_KEYS = checklistKeys_1.default; const commitKeys_1 = __importDefault(__nccwpck_require__(2546)); @@ -9688,6 +9688,8 @@ const inputKeys_1 = __importDefault(__nccwpck_require__(9913)); exports.INPUT_KEYS = inputKeys_1.default; const questions_1 = __importDefault(__nccwpck_require__(8080)); exports.QUESTIONS = questions_1.default; +const pullRequest_1 = __importDefault(__nccwpck_require__(577)); +exports.PULL_REQUEST = pullRequest_1.default; /***/ }), @@ -9703,11 +9705,38 @@ const INPUT_KEYS = Object.freeze({ ASSIGNEE_REQUIRED: 'assigneeRequired', CHECKLIST_REQUIRED: 'checklistRequired', SEMANTIC_TITLE_REQUIRED: 'semanticTitleRequired', + SEMANTIC_BRANCH_NAME_REQUIRED: 'semanticBranchNameRequired', REPO_TOKEN: 'repoToken' }); exports["default"] = INPUT_KEYS; +/***/ }), + +/***/ 577: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const PULL_REQUEST = Object.freeze({ + PREFIXES: Object.freeze([ + 'build', + 'chore', + 'ci', + 'docs', + 'feature', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ]) +}); +exports["default"] = PULL_REQUEST; + + /***/ }), /***/ 8080: @@ -9779,12 +9808,14 @@ function useInputs() { const isAssigneeRequired = Boolean(core.getInput(constants_1.INPUT_KEYS.ASSIGNEE_REQUIRED, { required: true })); const isChecklistRequired = Boolean(core.getInput(constants_1.INPUT_KEYS.CHECKLIST_REQUIRED, { required: true })); const isSemanticTitleRequired = Boolean(core.getInput(constants_1.INPUT_KEYS.SEMANTIC_TITLE_REQUIRED, { required: true })); + const isSemanticBranchNameRequired = Boolean(core.getInput(constants_1.INPUT_KEYS.SEMANTIC_BRANCH_NAME_REQUIRED, { required: true })); const repoToken = core.getInput(constants_1.INPUT_KEYS.REPO_TOKEN, { required: true }); return { isReviewerRequired, isAssigneeRequired, isChecklistRequired, isSemanticTitleRequired, + isSemanticBranchNameRequired, repoToken }; } @@ -9891,6 +9922,10 @@ async function commentErrors(errors) { await commentDraftPR(); return; } + if (await lib_1.pullRequest.missingSemanticBranchName()) { + await commentAndClosePR(); + return; + } await removeOldPRComments(); const octokit = (0, hooks_1.useOctokit)(); const checklistErrors = lib_1.checklist.extractErrorMessages(); @@ -9922,6 +9957,20 @@ async function commentDraftPR() { body: `BOT MESSAGE :robot:\n\n\nPullMate skips the checklist for draft PRs :construction:` }); } +async function commentAndClosePR() { + const octokit = (0, hooks_1.useOctokit)(); + const { PROwner } = await lib_1.pullRequest.getPRInfo(); + await octokit.rest.issues.createComment({ + ...github.context.repo, + issue_number: github.context.issue.number, + body: `BOT MESSAGE :robot:\n\n\nPlease follow the semantic branch naming convention :construction:\n\n\n@${PROwner}` + }); + await octokit.rest.pulls.update({ + ...github.context.repo, + pull_number: github.context.issue.number, + state: 'closed' + }); +} exports["default"] = { commentErrors }; @@ -10174,6 +10223,7 @@ async function getPRInfo() { isAssigned: !!issue?.data?.assignee, hasReviewers: !!PR?.data?.requested_reviewers?.length, PROwner: PR?.data?.user?.login ?? '', + branchName: PR?.data?.head?.ref ?? '', isMerged: PR?.data?.merged_at !== null }; } @@ -10216,13 +10266,32 @@ async function missingReviewers() { } return !hasReviewers; } +async function missingSemanticBranchName() { + const { isSemanticBranchNameRequired } = (0, hooks_1.useInputs)(); + const { branchName } = await getPRInfo(); + if (!isSemanticBranchNameRequired) { + return false; + } + return isInvalidBranchName(branchName); +} +function isInvalidBranchName(title) { + if (!constants_1.PULL_REQUEST.PREFIXES.some(prefix => title.startsWith(prefix))) { + return true; + } + const titleRegex = /^[a-z]+(?:\/[a-z-]+)+$/; + if (!titleRegex.test(title)) { + return true; + } + return false; +} exports["default"] = { getPRInfo, hasSemanticTitle, hasTaskNumber, missingAssignees, missingSemanticTitle, - missingReviewers + missingReviewers, + missingSemanticBranchName }; diff --git a/src/constants/index.ts b/src/constants/index.ts index 40a8a07..39ac1f3 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -2,5 +2,6 @@ import CHECKLIST_KEYS from './checklistKeys'; import COMMIT_KEYS from './commitKeys'; import INPUT_KEYS from './inputKeys'; import QUESTIONS from './questions'; +import PULL_REQUEST from './pullRequest'; -export { CHECKLIST_KEYS, COMMIT_KEYS, INPUT_KEYS, QUESTIONS }; +export { CHECKLIST_KEYS, COMMIT_KEYS, INPUT_KEYS, QUESTIONS, PULL_REQUEST }; diff --git a/src/constants/inputKeys.ts b/src/constants/inputKeys.ts index 534704c..86c3df0 100644 --- a/src/constants/inputKeys.ts +++ b/src/constants/inputKeys.ts @@ -3,6 +3,7 @@ const INPUT_KEYS = Object.freeze({ ASSIGNEE_REQUIRED: 'assigneeRequired', CHECKLIST_REQUIRED: 'checklistRequired', SEMANTIC_TITLE_REQUIRED: 'semanticTitleRequired', + SEMANTIC_BRANCH_NAME_REQUIRED: 'semanticBranchNameRequired', REPO_TOKEN: 'repoToken' }); diff --git a/src/constants/pullRequest.ts b/src/constants/pullRequest.ts new file mode 100644 index 0000000..4294cb4 --- /dev/null +++ b/src/constants/pullRequest.ts @@ -0,0 +1,17 @@ +const PULL_REQUEST = Object.freeze({ + PREFIXES: Object.freeze([ + 'build', + 'chore', + 'ci', + 'docs', + 'feature', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ]) +}); + +export default PULL_REQUEST; diff --git a/src/hooks/useInputs.ts b/src/hooks/useInputs.ts index 16e352f..e2ccb67 100644 --- a/src/hooks/useInputs.ts +++ b/src/hooks/useInputs.ts @@ -7,6 +7,7 @@ type UseInputsReturnTypes = { isAssigneeRequired: boolean; isChecklistRequired: boolean; isSemanticTitleRequired: boolean; + isSemanticBranchNameRequired: boolean; repoToken: string; }; @@ -27,6 +28,10 @@ export default function useInputs(): UseInputsReturnTypes { core.getInput(INPUT_KEYS.SEMANTIC_TITLE_REQUIRED, { required: true }) ); + const isSemanticBranchNameRequired = Boolean( + core.getInput(INPUT_KEYS.SEMANTIC_BRANCH_NAME_REQUIRED, { required: true }) + ); + const repoToken = core.getInput(INPUT_KEYS.REPO_TOKEN, { required: true }); return { @@ -34,6 +39,7 @@ export default function useInputs(): UseInputsReturnTypes { isAssigneeRequired, isChecklistRequired, isSemanticTitleRequired, + isSemanticBranchNameRequired, repoToken }; } diff --git a/src/lib/bot.ts b/src/lib/bot.ts index d6077e6..664f338 100644 --- a/src/lib/bot.ts +++ b/src/lib/bot.ts @@ -36,6 +36,11 @@ async function commentErrors(errors: string[]) { return; } + if (await pullRequest.missingSemanticBranchName()) { + await commentAndClosePR(); + return; + } + await removeOldPRComments(); const octokit = useOctokit(); @@ -76,4 +81,21 @@ async function commentDraftPR() { }); } +async function commentAndClosePR() { + const octokit = useOctokit(); + const { PROwner } = await pullRequest.getPRInfo(); + + await octokit.rest.issues.createComment({ + ...github.context.repo, + issue_number: github.context.issue.number, + body: `BOT MESSAGE :robot:\n\n\nPlease follow the semantic branch naming convention :construction:\n\n\n@${PROwner}` + }); + + await octokit.rest.pulls.update({ + ...github.context.repo, + pull_number: github.context.issue.number, + state: 'closed' + }); +} + export default { commentErrors }; diff --git a/src/lib/pullRequest.ts b/src/lib/pullRequest.ts index 69444a5..b8812b8 100644 --- a/src/lib/pullRequest.ts +++ b/src/lib/pullRequest.ts @@ -2,7 +2,7 @@ import * as github from '@actions/github'; import * as core from '@actions/core'; import { useOctokit, useInputs } from '@app/hooks'; -import { COMMIT_KEYS } from '@app/constants'; +import { COMMIT_KEYS, PULL_REQUEST } from '@app/constants'; async function getPRInfo() { const octokit = useOctokit(); @@ -24,6 +24,7 @@ async function getPRInfo() { isAssigned: !!issue?.data?.assignee, hasReviewers: !!PR?.data?.requested_reviewers?.length, PROwner: PR?.data?.user?.login ?? '', + branchName: PR?.data?.head?.ref ?? '', isMerged: PR?.data?.merged_at !== null }; } @@ -83,11 +84,37 @@ async function missingReviewers() { return !hasReviewers; } +async function missingSemanticBranchName() { + const { isSemanticBranchNameRequired } = useInputs(); + const { branchName } = await getPRInfo(); + + if (!isSemanticBranchNameRequired) { + return false; + } + + return isInvalidBranchName(branchName); +} + +function isInvalidBranchName(title: string) { + if (!PULL_REQUEST.PREFIXES.some(prefix => title.startsWith(prefix))) { + return true; + } + + const titleRegex = /^[a-z]+(?:\/[a-z-]+)+$/; + + if (!titleRegex.test(title)) { + return true; + } + + return false; +} + export default { getPRInfo, hasSemanticTitle, hasTaskNumber, missingAssignees, missingSemanticTitle, - missingReviewers + missingReviewers, + missingSemanticBranchName };