UNPKG

relion

Version:

Release workflow helper for Node.js projects.

202 lines (174 loc) 5.73 kB
import compareFunc from 'compare-func' import { DEFAULT_COMMIT_TYPES } from './constants.js' import { main, commit, header, footer } from './templates/index.js'; const COMMIT_HASH_LENGTH = 7 const releaseAsRegex = /release-as:\s*\w*@?([0-9]+\.[0-9]+\.[0-9a-z]+(-[0-9a-z.]+)?)\s*/i /** * Handlebar partials for various property substitutions based on commit context. */ const owner = '{{#if this.owner}}{{~this.owner}}{{else}}{{~@root.owner}}{{/if}}' const host = '{{~@root.host}}' const repository = '{{#if this.repository}}{{~this.repository}}{{else}}{{~@root.repository}}{{/if}}' export async function createWriterOpts(config) { const finalConfig = { types: DEFAULT_COMMIT_TYPES, issueUrlFormat: '{{host}}/{{owner}}/{{repository}}/issues/{{id}}', commitUrlFormat: '{{host}}/{{owner}}/{{repository}}/commit/{{hash}}', compareUrlFormat: '{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}', userUrlFormat: '{{host}}/{{user}}', issuePrefixes: ['#'], ...config } const commitUrlFormat = expandTemplate(finalConfig.commitUrlFormat, { host, owner, repository }) const compareUrlFormat = expandTemplate(finalConfig.compareUrlFormat, { host, owner, repository }) const issueUrlFormat = expandTemplate(finalConfig.issueUrlFormat, { host, owner, repository, id: '{{this.issue}}', prefix: '{{this.prefix}}' }) const writerOpts = getWriterOpts(finalConfig) writerOpts.mainTemplate = main writerOpts.headerPartial = header .replace(/{{compareUrlFormat}}/g, compareUrlFormat) writerOpts.commitPartial = commit .replace(/{{commitUrlFormat}}/g, commitUrlFormat) .replace(/{{issueUrlFormat}}/g, issueUrlFormat) writerOpts.footerPartial = footer .replace(/{{compareUrlFormat}}/g, compareUrlFormat) return writerOpts } function getWriterOpts(config) { const commitGroupOrder = config.types.flatMap(t => t.section).filter(t => t) return { finalizeContext: (context, _writerOpts, _filteredCommits, keyCommit) => { if (keyCommit) { const versionTagRegex = /tag:\s*([^,\s)]+)/i const keyCommitTag = keyCommit.gitTags?.match(versionTagRegex) if (keyCommitTag) { context.currentTag = keyCommitTag[1]; const currentTagIndex = context.gitSemverTags.indexOf(context.currentTag); context.previousTag = (currentTagIndex === -1) ? null : context.gitSemverTags[currentTagIndex + 1]; } } else { context.currentTag = context.newTag || null; context.previousTag = context.gitSemverTags[0] || null; } return context; }, transform: (commit, context) => { let discard = true const issues = [] const entry = findTypeEntry(config.types, commit) // Add an entry in the CHANGELOG if special Release-As footer // is used: if ((commit.footer && releaseAsRegex.test(commit.footer)) || (commit.body && releaseAsRegex.test(commit.body))) { discard = false } const notes = commit.notes.map((note) => { discard = false return { ...note, title: 'BREAKING CHANGES' } }) // breaking changes attached to any type are still displayed. if (discard && (entry === undefined || entry.hidden)) { return undefined } const type = entry ? entry.section : commit.type const scope = commit.scope === '*' ? '' : commit.scope const shortHash = typeof commit.hash === 'string' ? commit.hash.substring(0, COMMIT_HASH_LENGTH) : commit.shortHash let { subject } = commit if (typeof subject === 'string') { // Issue URLs. const issueRegEx = `(${config.issuePrefixes.join('|')})([a-z0-9]+)` const re = new RegExp(issueRegEx, 'g') subject = subject.replace(re, (_, prefix, issue) => { issues.push(prefix + issue) const url = expandTemplate(config.issueUrlFormat, { host: context.host, owner: context.owner, repository: context.repository, id: issue, prefix }) return `[${prefix}${issue}](${url})` }) // User URLs. subject = subject.replace(/\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/g, (_, user) => { // TODO: investigate why this code exists. if (user.includes('/')) { return `@${user}` } const usernameUrl = expandTemplate(config.userUrlFormat, { host: context.host, owner: context.owner, repository: context.repository, user }) return `[@${user}](${usernameUrl})` }) } // remove references that already appear in the subject const references = commit.references.filter(reference => !issues.includes(reference.prefix + reference.issue)) return { notes, type, scope, shortHash, subject, references } }, groupBy: 'type', // the groupings of commit messages, e.g., Features vs., Bug Fixes, are // sorted based on their probable importance: commitGroupsSort: (a, b) => { const gRankA = commitGroupOrder.indexOf(a.title) const gRankB = commitGroupOrder.indexOf(b.title) return gRankA - gRankB }, commitsSort: ['scope', 'subject'], noteGroupsSort: 'title', notesSort: compareFunc } } function findTypeEntry(types, commit) { const typeKey = (commit.revert ? 'revert' : commit.type || '').toLowerCase() return types.find((entry) => { if (entry.type !== typeKey) { return false } if (entry.scope && entry.scope !== commit.scope) { return false } return true }) } // expand on the simple mustache-style templates supported in // configuration (we may eventually want to use handlebars for this). function expandTemplate(template, context) { let expanded = template Object.keys(context).forEach((key) => { expanded = expanded.replace(new RegExp(`{{${key}}}`, 'g'), context[key]) }) return expanded }