UNPKG

@apolitical/mrlint

Version:

Node.js library to mantain GitLab MRs linted within a set of rules

186 lines (164 loc) 6.89 kB
const { danger, fail, warn, message, schedule } = require('danger'); var includes = require('lodash.includes'); var isEqual = require('lodash.isequal'); const { istanbulCoverage } = require('danger-plugin-istanbul-coverage'); const { default: npmOutdated } = require('danger-plugin-npm-outdated'); const { default: yarn } = require('danger-plugin-yarn'); /** * Rule: Ensure the MR body contains a link to Lighthouse reports * Reason: When working on a UI we need to ensure that there are no changes that introduce a regression in performance. */ if (danger.gitlab.metadata.repoSlug.includes('frontends')) { const repoName = danger.gitlab.metadata.repoSlug.split('/').at(-1); message( `💡 Did you know? 💡 You can see how are your changes affecting the performance by using [Lighthouse reports](https://lighthouse.apolitical.co/app/projects/${repoName}/dashboard).`, ); } /** * Rule: Ensure the MR has a reviewer and an assignee * Reason: The review flow for an MR is easier if this policy is followed. */ if (!danger.gitlab.mr.assignee) { const method = danger.gitlab.mr.work_in_progress || danger.gitlab.mr.title.includes('WIP') ? warn : fail; method('This pull request needs an assignee, and optionally include any reviewers.'); } /** * Rule: Changelog must be updated * Reason: We need to keep track of the changes performed on the repo */ const changelog = danger.git.fileMatch('CHANGELOG.md'); if (!changelog.edited) { fail(`📜 The CHANGELOG has not been updated yet!`); } /** * Rule: Chart YAML must be updated, only on frontends and apis * Reason: Versioning on Chart YAML file is important for new releases */ if (!danger.gitlab.metadata.repoSlug.includes('node-modules')) { const chartYaml = danger.git.fileMatch('Chart.yaml'); if (!chartYaml.edited) { fail(`📜 The Chart.yaml file has not been updated yet!`); } } /** * Rule: Suggest addition to labels to the MR and also Apolitibot usage if there's no versioning * Reason: It helps adding labels to MR so these can be grouped and filtered */ const mrLabels = danger.gitlab.mr.labels; if (mrLabels.length == 0) { warn(`🏷 Adding labels to MRs make it easier to group and find them, let's add one!`); message( `🤖 Did you know that Apolitibot can help you with project versioning? 🤖 Simply add any of the following labels ~versioning::patch ~versioning::minor ~versioning::major and we'll take care of versioning for you!`, ); } else if (mrLabels.length > 0) { if (!mrLabels.includes('~versioning::patch', '~versioning::minor', '~versioning::major')) { message( `🤖 Did you know that Apolitibot can help you with project versioning? 🤖 Simply add any of the following labels ~versioning::patch ~versioning::minor ~versioning::major and we'll take care of versioning for you!`, ); } } /** * Rule: Ensure the MR body contains a link to a gitlab issue * Reason: It's the most efficient way to jump from GitLab to issues to see what is being worked on. */ const mrBody = danger.gitlab.mr.description; const issuesUrlPattern = /https:\/\/gitlab\.com\/apolitical\/issues\/-\/issues\/(\d+)/g; if (!issuesUrlPattern.test(mrBody)) { fail( `🔍 I can't find any GitLab issue linked in the MR body. Please add a link to the GitLab issue, it's the most efficient way to jump to the corresponding ticket 🏎`, ); } /** * Rule: Ensure that lockfile is updated alognside package.json * Reason: yarn.lock must be updated if package.json dependencies are modified */ schedule(async () => { const packageDiff = await danger.git.JSONDiffForFile('package.json'); const hasPackageChanges = includes(danger.git.modified_files, 'package.json'); const hasLockfileChanges = includes(danger.git.modified_files, 'yarn.lock'); if (hasPackageChanges) { var updatedDependencies = null; var updatedDevDependencies = null; if (packageDiff.dependencies) { updatedDependencies = !isEqual(packageDiff.dependencies.before, packageDiff.dependencies.after); } if (packageDiff.devDependencies) { updatedDevDependencies = !isEqual(packageDiff.devDependencies.before, packageDiff.devDependencies.after); } if ((updatedDependencies || updatedDevDependencies) && !hasLockfileChanges) { fail('There are package.json changes with no corresponding lockfile changes'); } } }); /** * Rule: Ensure that there are no outdated packages * Reason: As releases are happening quite often, its desirable to be on the latest version. */ schedule(npmOutdated()); /** * Rule: Check if there are newly added packages * Reason: It's a nice way of seeing if any new dependency is added to the repo */ schedule(yarn()); /** * Rule: Ensure the MR contains tests that are above a certain threshold * Reason: It is mandatory that new features or new files contain tests paired with them. */ schedule( istanbulCoverage({ coveragePath: { path: './coverage/lcov.info', type: 'lcov' }, numberOfEntries: 6, threshold: { statements: 90, branches: 90, functions: 90, lines: 90, }, }), ); /** * Rule: Ensure that there aren't any console.log commands added * Reason: These commands are great for debugging, but for prod we send logs to Google Stackdriver. */ const PATTERN = /console\.(log|error|warn|info)/; const GLOBAL_PATTERN = new RegExp(PATTERN.source, 'g'); const JS_FILE = /\.(js|ts)x?$/i; const findConsole = (content, whitelist) => { let matches = content.match(GLOBAL_PATTERN); if (!matches) return []; matches = matches.filter((match) => { const singleMatch = PATTERN.exec(match); if (!singleMatch || singleMatch.length === 0) return false; return !whitelist.includes(singleMatch[1]); }); return matches; }; const defaultCallback = (file, matches) => fail(`${matches.length} console statement(s) added in ${file}.`); /** * Danger plugin to prevent merging code that still has `console.log`s inside it. */ async function noConsole(options = {}) { const whitelist = options.whitelist || []; const callback = options.callback || defaultCallback; if (!Array.isArray(whitelist)) throw new Error('[danger-plugin-no-console] whitelist option has to be an array.'); if (typeof callback !== 'function') throw new Error('[danger-plugin-no-console] callback option has to be an function.'); const diffs = danger.git.created_files .concat(danger.git.modified_files) .filter((file) => JS_FILE.test(file)) .map((file) => { return danger.git.diffForFile(file).then((diff) => ({ file, diff, })); }); const additions = await Promise.all(diffs); additions .filter(({ diff }) => !!diff) .forEach(({ file, diff }) => { const matches = findConsole(diff.added, whitelist); if (matches.length === 0) return; callback(file, matches); }); } schedule(noConsole());