UNPKG

prcolinter

Version:

Easily lint each commit for your PRs against the Conventional Commit spec based on configurable Linter rules.

253 lines (211 loc) 8.24 kB
'use strict'; /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ const lintLib = require('@commitlint/lint'); const defaultConfig = require('@commitlint/config-conventional'); const core = require('@actions/core'); const github = require('@actions/github'); const fs = require('fs'); const path = require('path'); const moment = require('moment'); const lint = lintLib.default; const defaultConfigRules = defaultConfig.rules; const validEvent = ['pull_request']; async function run() { try { const token = core.getInput('token', { required: true }); const createComment = core.getBooleanInput('comment'); const deleteComment = core.getBooleanInput('delete_comment'); // load rules from file, if there const configPath = core.getInput('config_path', { required: true }); // relative to dist/index.js const filename = path.join(__dirname, '/../', configPath); core.debug(`Loading config from path: ${filename}`); const config = fs.existsSync(filename) ? JSON.parse(fs.readFileSync(filename, 'utf8')) : {}; core.debug(`Loaded config is: ${JSON.stringify(config)}`); const fileRules = config.rules || {}; // load raw rules from action, if there const rulesRaw = core.getInput('rules'); const yamlRules = rulesRaw ? JSON.parse(rulesRaw) : {}; core.debug(`Default rules: ${JSON.stringify(defaultConfigRules)}`); core.debug(`File rules: ${JSON.stringify(fileRules)}`); core.debug(`Yaml rules: ${JSON.stringify(yamlRules)}`); const ruleSet = { ...defaultConfigRules, ...fileRules, ...yamlRules, }; core.debug(`Final rules: ${JSON.stringify(ruleSet)}`); const octokit = new github.getOctokit(token); const { eventName, payload: { repository: repo, pull_request: pr, }, issue: { number: num, }, repo: { owner, }, } = github.context; core.debug(`Context ---- payload.repository: ${JSON.stringify(repo)}`); core.debug(`Context ---- payload.pull_request: ${JSON.stringify(pr)}`); core.debug(`Context ---- issue.number: ${JSON.stringify(num)}`); core.debug(`Context ---- repo.owner: ${JSON.stringify(owner)}`); if (validEvent.indexOf(eventName) < 0) { core.error(`Invalid event: ${eventName}`); return; } core.debug(`Fetching commits for PR #${num}...`); const commits = await octokit.rest.pulls.listCommits({ owner: repo.owner.login, repo: repo.name, pull_number: pr.number, }); core.debug(`Processing ${commits.data.length} commits...`); const reports = await Promise.all(commits.data.map(commit => lint(commit.commit.message, ruleSet))); core.debug(`Report results: ${JSON.stringify(reports)}`); let countErrors = 0; let countWarnings = 0; const authors = []; const commitReports = []; reports.forEach((report, i) => { const meta = commits.data[i]; const { sha, commit } = meta; if (!authors.includes(commit.author.name)) { authors.push(commit.author.name); } const shaShort = sha.substring(0, 7); const relativeTime = moment(commit.author.date).fromNow(); core.debug(` Commit ${shaShort}`); const msg = `Commit "${commit.message}" ${shaShort} (${commit.author.name} <${commit.author.email}> on ${commit.author.date})`; core.startGroup(msg); core.debug(msg); if (!meta.committer) { core.warning(`Commit "${commit.message}" ${shaShort} has no committer in metadata.`); } // handle missing committer due to deleted account, etc. const commiterInfo = meta.committer ? `By **[${commit.author.name} (${meta.committer.login})](https://github.com/${meta.committer.login})** _${relativeTime}_` : `By **${commit.author.name} (Unknown Login) _${relativeTime}_`; const headerIcon = report.valid ? '✅' : report.errors.length ? '❌' : '⚠️'; let commitReportText = ` ### ${headerIcon} [Commit ${shaShort}](https://github.com/${owner}/${repo.name}/commit/${sha}) ${commiterInfo} \`\`\` ${commit.message} \`\`\` `; if (!report.valid) { let errorReportText = ''; report.errors.forEach((err) => { const ruleDef = ruleSet[err.name]; core.error(`Rule '${err.name}': ${err.message} ("${commit.message}")`); countErrors++; errorReportText += ` ❌ **ERROR**: ${err.message} > ["${err.name}"](https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-rules.md#${err.name}): ${JSON.stringify(ruleDef)} `; }); let warningReportText = ''; report.warnings.forEach((err) => { const ruleDef = ruleSet[err.name]; core.warning(`Rule '${err.name}': ${err.message} ("${commit.message}")`); countWarnings++; warningReportText += ` ⚠️ **Warning**: ${err.message} > ["${err.name}"](https://github.com/conventional-changelog/commitlint/blob/master/docs/reference-rules.md#${err.name}): ${JSON.stringify(ruleDef)} `; }); commitReportText += ` ${errorReportText} ${warningReportText} `; } commitReports.push(commitReportText); core.endGroup(); }); if (countErrors) { core.setFailed(`Action failed with ${countErrors} errors (and ${countWarnings} warnings)`); } if (deleteComment) { const perPage = 100; let page = 1; let hasMore = true; do { core.debug(`Fetching page #${page} (max: ${perPage}) of existing comments...`); const res = await octokit.rest.issues.listComments({ owner, repo: repo.name, issue_number: num, per_page: 100, page, }); const relevantComments = res.data.filter(comment => comment.body.includes('## Commit Message Lint Report')); core.debug(`Fetched ${relevantComments.length} relevant (previous lint report) comments...`); for (let i = 0; i < relevantComments.length; i++) { core.debug(`Deleting comment #${relevantComments[i].id}...`); await octokit.rest.issues.deleteComment({ owner, repo: repo.name, comment_id: relevantComments[i].id, }); } page++; if (res.data.length < perPage) { hasMore = false; } } while (hasMore); } if (createComment) { if (countErrors || countWarnings) { const finalReport = ` # 🚨🚔 Unconventional Commit 👮‍♀️🙅‍♂️ 🤖 Beep boop! Looks like one or more of your commit messages wasn't quite right. ## Commit Message Lint Report - ✏️ **${commits.data.length} commit(s)** - 👤 **${authors.length} author(s)** - ❌ **${countErrors} lint error(s)** - ⚠️ **${countWarnings} lint warning(s)** ${commitReports.join('\n')} ## Tips Be sure to follow the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) guideline when authoring your commits. If your most recent commit is to blame, you can edit a single commit with: \`\`\` git commit --amend \`\`\` To edit & merge multiple commits, you can rebase with \`git rebase -i master\` (be sure your master is up to date). `; await octokit.rest.issues.createComment({ owner, repo: repo.name, issue_number: num, body: finalReport, }); } else { const finalReport = ` # ✅🙏🏻 Conventional Commit 🥳 🎉 🤖 Beep boop! Congrats, it like all your commit messages conform to the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) spec! 👏👏👏 Your PR can be closed. Coffee is for closers, so here's a coffee for you: ☕️ ## Commit Message Lint Report - ✏️ **${commits.data.length} commit(s)** - 👤 **${authors.length} author(s)** - ❌ **${countErrors} lint error(s)** - ⚠️ **${countWarnings} lint warning(s)** `; await octokit.rest.issues.createComment({ owner, repo: repo.name, issue_number: num, body: finalReport, }); } } } catch (err) { console.error(err); core.setFailed(err.message); } } run();