UNPKG

@aritslimited/commitlint

Version:

A commit linting commitizen adapter & branch naming convention tool tailored for ARITS Limited with Jira Issue & Project Tracking Software; to track commits to Jira issues and transition them to the next stage of development workflow automatically.

467 lines (466 loc) 20 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); require("./dotenv.config.js"); const child_process_1 = require("child_process"); const branchlint_config_js_1 = require("./branchlint.config.js"); const LimitedInputPrompt_1 = require("./Components/LimitedInputPrompt"); const requiredEnvVariables = [ "JIRA_BASE_URL", "JIRA_PROJECT", "JIRA_ISSUE_FILTERS", "JIRA_ISSUE_TRANSITION_FILTERS", "JIRA_API_USER", "JIRA_API_TOKEN", ]; const env = tslib_1.__rest(Object.fromEntries(requiredEnvVariables.map((varName) => { const value = process.env[varName]; if (value === undefined) { throw new Error(`Environment variable '${varName}' is not defined.`); } return [varName, value]; })), []); const MAX_SUMMARY_LENGTH = 50; const MAX_TITLE_WIDTH = 120; const base64Credentials = Buffer.from(`${env.JIRA_API_USER}:${env.JIRA_API_TOKEN}`).toString("base64"); const fetchJiraIssues = () => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const status = env.JIRA_ISSUE_FILTERS.split(", ") .map((e) => `"${e.replace(/\[|\]/g, "")}"`) .join(", "); const jqlQuery = `project = "${env.JIRA_PROJECT}" AND status IN (${status}) AND assignee = currentUser() ORDER BY updated DESC`; const jiraIssues = [ { value: undefined, name: "Undefined issue! Task first, commit next!" }, ]; try { const response = yield fetch(`${env.JIRA_BASE_URL}/search?jql=${encodeURIComponent(jqlQuery)}`, { method: "GET", headers: { Authorization: `Basic ${base64Credentials}`, "Content-Type": "application/json", Accept: "application/json", }, }); if (response.ok) { const { issues } = yield response.json(); if (issues) { jiraIssues.unshift(issues.map((issue) => ({ value: issue.key, priority: issue.fields.priority.name, name: `${issue.key}${issue.fields.issuetype.name} [${issue.fields.priority.name}]: ${issue.fields.summary.length > MAX_SUMMARY_LENGTH ? issue.fields.summary.slice(0, MAX_SUMMARY_LENGTH) + "..." : issue.fields.summary}`, id: issue.id, }))); return jiraIssues.flat(); } } throw new Error(`Error fetching Jira issues: ${response.status}`); } catch (error) { console.error(error); } return jiraIssues; }); const fetchJiraIssueTransitions = (jiraIssueKey) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const jiraIssueTransitionFilters = env.JIRA_ISSUE_TRANSITION_FILTERS.split(", ").map((e) => e.replace(/\[|\]/g, "")); const jiraIssueTransitions = [{ value: undefined, name: "Keep issue status unchanged!" }]; try { const response = yield fetch(`${env.JIRA_BASE_URL}/issue/${jiraIssueKey}/transitions`, { method: "GET", headers: { Authorization: `Basic ${base64Credentials}`, "Content-Type": "application/json", Accept: "application/json", }, }); if (response.ok) { const { transitions } = yield response.json(); if (transitions) { const transition = transitions.map((transition) => { var _a; if (jiraIssueTransitionFilters.includes(transition.name)) { return { value: transition.id, name: `${transition.name} [${(_a = transition.to.self.name) !== null && _a !== void 0 ? _a : transition.to.name}]`, }; } return null; }); jiraIssueTransitions.unshift(transition.filter(Boolean)); return jiraIssueTransitions.flat(); } } throw new Error(`Error fetching Jira issue transitions: ${response.status}`); } catch (error) { console.error(error); } return jiraIssueTransitions; }); const updateJiraIssueStatus = (jiraIssueKey, transitionId) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const { green } = (yield import("chalk")).default; const response = yield fetch(`${env.JIRA_BASE_URL}/issue/${jiraIssueKey}/transitions`, { method: "POST", headers: { Authorization: `Basic ${base64Credentials}`, "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ transition: { id: transitionId, }, }), }); if (response.ok) { return green("\t✓ Issue status updated successfully!"); } throw new Error(`Error updating Jira issue status for ${jiraIssueKey}: ${response.status}`); }); const updateJiraIssueTimeTracking = (jiraIssueKey, timeSpent) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const { green } = (yield import("chalk")).default; const response = yield fetch(`${env.JIRA_BASE_URL}/issue/${jiraIssueKey}/worklog`, { method: "POST", headers: { Authorization: `Basic ${base64Credentials}`, "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ timeSpent, }), }); if (response.ok) { return green("\t✓ Work log updated successfully!"); } throw new Error(`Error updating Jira issue time tracking for ${jiraIssueKey}: ${response.status}`); }); const handleUpdateJiraIssue = (JIRA_ISSUE_KEYS, ISSUE_TRANSITION_IDs, TIME_TO_ACCOMPLISH_TASKS) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { const { blue } = (yield import("chalk")).default; JIRA_ISSUE_KEYS.filter(Boolean).map((jiraIssueKey) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { if (jiraIssueKey !== undefined && Object.keys(ISSUE_TRANSITION_IDs).includes(jiraIssueKey) && Object.keys(TIME_TO_ACCOMPLISH_TASKS).includes(jiraIssueKey)) { console.log(`\nUpdating status & work log of ${blue(jiraIssueKey)}...\n`, yield updateJiraIssueStatus(jiraIssueKey, ISSUE_TRANSITION_IDs[jiraIssueKey]), "\n", yield updateJiraIssueTimeTracking(jiraIssueKey, TIME_TO_ACCOMPLISH_TASKS[jiraIssueKey])); } })); }); import("inquirer/lib/prompts/input.js").then((m) => m.default); const prompter = (cz, commit) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { var _a, _b; process.env.VALID_BRANCH_NAMES && (yield (0, branchlint_config_js_1.branchLint)(process.env.VALID_BRANCH_NAMES)); cz.registerPrompt("limitedInput", yield (0, LimitedInputPrompt_1.LimitedInputPrompt)()); const binaryChoices = [ { value: true, name: "Yes" }, { value: false, name: "No" }, ]; const validBranchNames = (validBranchNameString) => validBranchNameString.split(/ /g).map((branch) => { branch = branch.replace(/[-_]/g, " "); return branch .split(/\s+/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); }); const multiChoiceSteps = { commitTypes: { question: "Select the type of update that you're committing:", choices: [ { value: "feat", name: "feat: A new feature" }, { value: "fix", name: "fix: A bug fix" }, { value: "chore", name: "chore: Continuation of an incomplete feature/other changes", }, { value: "docs", name: "docs: Documentation only changes" }, { value: "style", name: "style: CSS style changes" }, { value: "refactor", name: "refactor: A code update that neither fixes a bug nor adds a feature", }, { value: "perf", name: "perf: A code update that improves performance", }, { value: "test", name: "test: Adding test script" }, { value: "build", name: "build: Changes that affect the build system or external dependencies", }, { value: "ci", name: "ci: Changes that affect the continuous integration system", }, { value: "revert", name: "revert: Reverts a previous commit" }, ], }, jiraIssueKeys: { question: "Select the Jira issue key(s) that you're committing:", choices: yield fetchJiraIssues(), }, commitScopes: { question: "Select the scope of your changes:", choices: process.env.VALID_BRANCH_NAMES ? process.env.VALID_BRANCH_NAMES.split(/[\s,]+/).map((validBranch) => { return { value: validBranch, name: validBranchNames(validBranch), }; }) : (0, child_process_1.execSync)(`git for-each-ref --format="%(refname:short)" refs/heads/`) .toString() .trim() .split("\n") .map((vb) => { return { value: vb, name: validBranchNames(vb), }; }), }, isBreakingUpdate: { question: "Are there any breaking changes?", choices: binaryChoices, }, commitConfirmation: { question: "Are you sure you want to proceed with the commit?", choices: binaryChoices, }, pushCommitConfirmation: { question: "Do you want to push the commit to remote?", choices: binaryChoices, }, }; const getFormattedTitlePrefix = ({ type, scope, isBreaking, jiraIssueKey, }) => `${type}(${scope})${isBreaking ? "!" : ""}: `; const jiraIssueTransitions = {}; const { COMMIT_TYPE, JIRA_ISSUE_KEYS, } = yield cz.prompt([ { type: "list", name: "COMMIT_TYPE", message: multiChoiceSteps.commitTypes.question, choices: multiChoiceSteps.commitTypes.choices, when: true, default: (_a = multiChoiceSteps.commitTypes.choices.find((choice) => choice.value === "chore")) === null || _a === void 0 ? void 0 : _a.value, pageSize: 15, }, { type: "checkbox", name: "JIRA_ISSUE_KEYS", message: multiChoiceSteps.jiraIssueKeys.question, when: ({ COMMIT_TYPE, }) => { return (COMMIT_TYPE !== "build" && COMMIT_TYPE !== "ci" && COMMIT_TYPE !== "revert"); }, choices: multiChoiceSteps.jiraIssueKeys.choices, validate: (input) => { if (input.length < 1) return "You must select at least one issue key"; return true; }, }, ]); if (JIRA_ISSUE_KEYS && JIRA_ISSUE_KEYS.length > 0) { JIRA_ISSUE_KEYS.filter(Boolean).map((jiraIssueKey) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { Object.assign(jiraIssueTransitions, { [jiraIssueKey]: yield fetchJiraIssueTransitions(jiraIssueKey), }); })); } const TIME_TO_ACCOMPLISH_TASKS = tslib_1.__rest(yield cz.prompt(JIRA_ISSUE_KEYS && JIRA_ISSUE_KEYS.length > 0 ? JIRA_ISSUE_KEYS.filter(Boolean).map((jiraIssueKey) => ({ type: "input", name: `${jiraIssueKey}`, message: `Time taken for ${jiraIssueKey} (required):\n`, suffix: "{1w 2d 3h 4m}", when: ({ COMMIT_TYPE, }) => { return (COMMIT_TYPE !== "build" && COMMIT_TYPE !== "ci" && COMMIT_TYPE !== "revert"); }, validate(input) { if (input.length < 2) { return `The time must have at least ${2} characters`; } const unitOrder = { w: 1, d: 2, h: 3, m: 4 }; const units = input.match(/\d+[wdhm]/g); if (!units || (units === null || units === void 0 ? void 0 : units.length) === 0) { return "The time must contain at least one unit (w, d, h, m)"; } const seenUnits = new Set(); let prevUnitOrder = 0; for (const unit of units) { const unitType = unit.charAt(unit.length - 1); const unitValue = parseInt(unit, 10); if (unitValue === 0) { return "The time must not contain leading zero values"; } if (seenUnits.has(unitType)) { return "The time must not contain duplicate units"; } if (unitOrder[unitType] <= prevUnitOrder) { return "The time must be in descending order of units"; } seenUnits.add(unitType); prevUnitOrder = unitOrder[unitType]; } return true; }, filter(time) { const units = time.match(/\d+[wdhm]/g); if (!units || (units === null || units === void 0 ? void 0 : units.length) === 0) { throw Error("Invalid time format"); } return units.join(" "); }, })) : []), []); const ISSUE_TRANSITION_IDs = tslib_1.__rest(yield cz.prompt(JIRA_ISSUE_KEYS && JIRA_ISSUE_KEYS.length > 0 ? JIRA_ISSUE_KEYS.filter(Boolean).map((jiraIssueKey) => ({ type: "list", name: `${jiraIssueKey}`, message: `Update status of ${jiraIssueKey} to:`, choices: jiraIssueTransitions[jiraIssueKey], default: 0, })) : []), []); const { COMMIT_SCOPE, COMMIT_TITLE, COMMIT_BODY, IS_BREAKING_UPDATE, } = yield cz.prompt([ { type: "list", name: "COMMIT_SCOPE", message: multiChoiceSteps.commitScopes.question, choices: multiChoiceSteps.commitScopes.choices, when: true, default: () => { const currentBranch = (0, child_process_1.execSync)("git branch --show-current") .toString() .trim(); const defaultSelectedIndex = multiChoiceSteps.commitScopes.choices.findIndex((choice) => currentBranch.includes(choice.value)); if (defaultSelectedIndex !== -1) { return defaultSelectedIndex; } return 0; }, }, { type: "list", name: "IS_BREAKING_UPDATE", message: multiChoiceSteps.isBreakingUpdate.question, choices: multiChoiceSteps.isBreakingUpdate.choices, when: ({ COMMIT_TYPE, }) => { return (COMMIT_TYPE !== "build" && COMMIT_TYPE !== "ci" && COMMIT_TYPE !== "revert"); }, default: (_b = multiChoiceSteps.isBreakingUpdate.choices.find((choice) => !choice.value)) === null || _b === void 0 ? void 0 : _b.value, }, { type: "limitedInput", name: "COMMIT_TITLE", message: "Commit title — a short, imperative tense description (required):\n", when: true, maxLength: MAX_TITLE_WIDTH, leadingLabel: ({ COMMIT_SCOPE, IS_BREAKING_UPDATE, }) => { return getFormattedTitlePrefix({ type: COMMIT_TYPE, scope: COMMIT_SCOPE, isBreaking: IS_BREAKING_UPDATE, jiraIssueKey: JIRA_ISSUE_KEYS, }); }, validate: (input) => input.length >= 5 || `The title must have at least ${5} characters`, filter: (title) => { title = title.trim(); title = title.replace(/\s+$/, ""); return title; }, }, { type: "editor", name: "COMMIT_BODY", message: ({ IS_BREAKING_UPDATE, }) => { return IS_BREAKING_UPDATE ? "Provide a longer description of the breaking changes: (press Enter, write, then [Esc] + [:] + [wq] to save)\n" : "Provide a longer description: (press Enter, then [Esc] + [:] + [q] to skip)\n"; }, when: ({ COMMIT_TYPE, IS_BREAKING_UPDATE, }) => { return ((COMMIT_TYPE !== "build" && COMMIT_TYPE !== "ci" && COMMIT_TYPE !== "revert") || IS_BREAKING_UPDATE); }, validate: (input, { IS_BREAKING_UPDATE }) => { if (IS_BREAKING_UPDATE) { return (input.length >= 10 || `The body must have at least ${10} characters`); } return true; }, filter: (body) => { body = body.trim(); return body.length > 0 ? body : ""; }, }, ]); const bodyString = (COMMIT_BODY === null || COMMIT_BODY === void 0 ? void 0 : COMMIT_BODY.length) ? `\n\n\n${IS_BREAKING_UPDATE ? "BREAKING CHANGE:" : "DESCRIPTION:"}\n` + COMMIT_BODY + "\n" : "\n\n\n"; const contentModificationsString = `CONTENT MODIFICATIONS:\n${(0, child_process_1.execSync)("git diff --cached --name-status") .toString() .split("\n") .map((line) => `\t${line.trim()}`) .join("\n")}`; const timeString = Object.keys(TIME_TO_ACCOMPLISH_TASKS).length ? `\nTIME:\n${Object.entries(TIME_TO_ACCOMPLISH_TASKS) .map(([jiraIssueKey, time], index) => `\t${jiraIssueKey}\t[${time}]`) .join("\n")}` : ""; const commitMessage = `${getFormattedTitlePrefix({ type: COMMIT_TYPE, scope: COMMIT_SCOPE, isBreaking: IS_BREAKING_UPDATE, jiraIssueKey: JIRA_ISSUE_KEYS, })}${COMMIT_TITLE}` + bodyString + contentModificationsString + timeString; const boxen = (yield import("boxen")).default; const boxenOptions = { padding: 1, margin: 1, title: "Commit Message Preview", titleAlignment: "center", borderColor: "yellow", borderStyle: { topLeft: "", topRight: "", bottomLeft: "", bottomRight: "", top: "-", bottom: "-", left: "", right: "", }, float: "center", }; console.log(boxen(commitMessage.trim(), boxenOptions)); const { confirm } = yield cz.prompt([ { type: "list", name: "confirm", message: multiChoiceSteps.commitConfirmation.question, choices: multiChoiceSteps.commitConfirmation.choices, default: true, }, ]); if (!confirm) process.exit(1); else { commit(commitMessage.trim()); if (JIRA_ISSUE_KEYS && ISSUE_TRANSITION_IDs && TIME_TO_ACCOMPLISH_TASKS) { yield handleUpdateJiraIssue(JIRA_ISSUE_KEYS, ISSUE_TRANSITION_IDs, TIME_TO_ACCOMPLISH_TASKS); } } }); exports.default = { prompter };