@apolitical/mrlint
Version:
Node.js library to mantain GitLab MRs linted within a set of rules
186 lines (164 loc) • 6.89 kB
JavaScript
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());