|
| 1 | +// ? See https://github.com/conventional-changelog/conventional-changelog |
| 2 | + |
| 3 | +const debug = require('debug')( |
| 4 | + `${require('./package.json').name}:conventional-changelog-config` |
| 5 | +); |
| 6 | + |
| 7 | +const escapeRegExpStr = require('escape-string-regexp'); |
| 8 | +const semver = require('semver'); |
| 9 | +const sjx = require('shelljs'); |
| 10 | + |
| 11 | +// ? Commit types that trigger releases by default (using angular configuration) |
| 12 | +// ? See https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js |
| 13 | +const DEFAULT_RELEASED_TYPES = ['feat', 'fix', 'perf']; |
| 14 | + |
| 15 | +// ? Same options as commit-analyzer's releaseRules (see |
| 16 | +// ? https://github.com/semantic-release/commit-analyzer#releaserules) with the |
| 17 | +// ? addition of the `title` property to set the resulting section title |
| 18 | +const ADDITIONAL_RELEASE_RULES = [ |
| 19 | + { type: 'build', release: 'patch', title: 'Build System' } |
| 20 | +]; |
| 21 | + |
| 22 | +const changelogTitle = |
| 23 | + `# Changelog\n\n` + |
| 24 | + `All notable changes to this project will be documented in this file.\n\n` + |
| 25 | + `The format is based on [Conventional Commits](https://conventionalcommits.org),\n` + |
| 26 | + `and this project adheres to [Semantic Versioning](https://semver.org).`; |
| 27 | + |
| 28 | +// ? Strings in commit messages that, when found, are skipped |
| 29 | +// ! These also have to be updated in build-test-deploy.yml and cleanup.yml |
| 30 | +const SKIP_COMMANDS = '[skip ci], [ci skip], [skip cd], [cd skip]'.split(', '); |
| 31 | + |
| 32 | +debug('SKIP_COMMANDS=', SKIP_COMMANDS); |
| 33 | + |
| 34 | +sjx.config.silent = true; |
| 35 | + |
| 36 | +// ! XXX: dark magic to synchronously deal with this async package |
| 37 | +const wait = sjx.exec( |
| 38 | + `node -e 'require("conventional-changelog-angular").then(o => console.log(o.writerOpts.transform.toString()));'` |
| 39 | +); |
| 40 | + |
| 41 | +if (wait.code != 0) throw new Error('failed to acquire angular transformation'); |
| 42 | + |
| 43 | +const transform = Function(`"use strict";return (${wait.stdout})`)(); |
| 44 | +const sentenceCase = (s) => s.toString().charAt(0).toUpperCase() + s.toString().slice(1); |
| 45 | + |
| 46 | +const extraReleaseTriggerCommitTypes = ADDITIONAL_RELEASE_RULES.map((r) => r.type); |
| 47 | +const allReleaseTriggerCommitTypes = [ |
| 48 | + DEFAULT_RELEASED_TYPES, |
| 49 | + extraReleaseTriggerCommitTypes |
| 50 | +].flat(); |
| 51 | + |
| 52 | +debug('extra types that trigger releases = %O', extraReleaseTriggerCommitTypes); |
| 53 | +debug('all types that trigger releases = %O', allReleaseTriggerCommitTypes); |
| 54 | + |
| 55 | +// ? Releases made before this repo adopted semantic-release. They will be |
| 56 | +// ? collected together under a single header |
| 57 | +const legacyReleases = []; |
| 58 | +let shouldGenerate = true; |
| 59 | + |
| 60 | +module.exports = { |
| 61 | + changelogTitle, |
| 62 | + additionalReleaseRules: ADDITIONAL_RELEASE_RULES.map(({ title, ...r }) => r), |
| 63 | + parserOpts: { |
| 64 | + mergePattern: /^Merge pull request #(\d+) from (.*)$/, |
| 65 | + mergeCorrespondence: ['id', 'source'], |
| 66 | + noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'], |
| 67 | + // eslint-disable-next-line no-console |
| 68 | + warn: console.warn.bind(console) |
| 69 | + }, |
| 70 | + writerOpts: { |
| 71 | + generateOn: (commit) => { |
| 72 | + const decision = |
| 73 | + shouldGenerate === 'always' || |
| 74 | + (shouldGenerate && |
| 75 | + !!semver.valid(commit.version) && |
| 76 | + !semver.prerelease(commit.version)); |
| 77 | + debug(`::generateOn shouldGenerate=${shouldGenerate} decision=${decision}`); |
| 78 | + shouldGenerate = true; |
| 79 | + return decision; |
| 80 | + }, |
| 81 | + transform: (commit, context) => { |
| 82 | + const version = commit.version || null; |
| 83 | + const firstRelease = version === context.gitSemverTags?.slice(-1)[0].slice(1); |
| 84 | + |
| 85 | + debug('::transform encountered commit = %O', commit); |
| 86 | + debug(`::transform commit version = ${version}`); |
| 87 | + debug(`::transform commit firstRelease = ${firstRelease}`); |
| 88 | + |
| 89 | + if (commit.revert) { |
| 90 | + debug('::transform coercing to type "revert"'); |
| 91 | + commit.type = 'revert'; |
| 92 | + } else if (commit.type == 'revert') { |
| 93 | + debug('::transform ignoring malformed revert commit'); |
| 94 | + return null; |
| 95 | + } |
| 96 | + |
| 97 | + commit.originalType = commit.type; |
| 98 | + |
| 99 | + if (!firstRelease || commit.type) { |
| 100 | + // ? This commit does not have a type, but has a version. It must be a |
| 101 | + // ? legacy release! |
| 102 | + if (version && !commit.type) { |
| 103 | + debug('::transform determined commit is legacy release'); |
| 104 | + legacyReleases.push(commit); |
| 105 | + commit = null; |
| 106 | + shouldGenerate = false; |
| 107 | + } else { |
| 108 | + let fakeFix = false; |
| 109 | + |
| 110 | + if (extraReleaseTriggerCommitTypes.includes(commit.type)) { |
| 111 | + debug(`::transform encountered custom commit type: ${commit.type}`); |
| 112 | + commit.type = 'fix'; |
| 113 | + fakeFix = true; |
| 114 | + } |
| 115 | + |
| 116 | + commit = transform(commit, context); |
| 117 | + |
| 118 | + debug('::transform angular transformed commit = %O', commit); |
| 119 | + |
| 120 | + if (commit) { |
| 121 | + if (fakeFix) { |
| 122 | + commit.type = ADDITIONAL_RELEASE_RULES.find( |
| 123 | + (r) => r.type == commit.originalType |
| 124 | + )?.title; |
| 125 | + debug('::transform debug: %O', ADDITIONAL_RELEASE_RULES); |
| 126 | + debug(`::transform commit type set to custom title: ${commit.type}`); |
| 127 | + } else commit.type = sentenceCase(commit.type); |
| 128 | + |
| 129 | + // ? Ignore any commits with skip commands in them |
| 130 | + if (SKIP_COMMANDS.some((cmd) => commit.subject?.includes(cmd))) { |
| 131 | + debug(`::transform saw skip command in commit message; commit skipped`); |
| 132 | + return null; |
| 133 | + } |
| 134 | + |
| 135 | + if (commit.subject) { |
| 136 | + // ? Make scope-less commit subjects sentence case in the |
| 137 | + // ? changelog per my tastes |
| 138 | + if (!commit.scope) commit.subject = sentenceCase(commit.subject); |
| 139 | + |
| 140 | + // ? Italicize reverts per my tastes |
| 141 | + if (commit.originalType == 'revert') commit.subject = `*${commit.subject}*`; |
| 142 | + } |
| 143 | + |
| 144 | + // ? For breaking changes, make all scopes and subjects bold. |
| 145 | + // ? Scope-less subjects are made sentence case. All per my |
| 146 | + // ? tastes |
| 147 | + commit.notes.forEach((note) => { |
| 148 | + if (note.text) { |
| 149 | + debug('::transform saw BC notes for this commit'); |
| 150 | + const [firstLine, ...remainder] = note.text.trim().split('\n'); |
| 151 | + note.text = |
| 152 | + `**${!commit.scope ? sentenceCase(firstLine) : firstLine}**` + |
| 153 | + remainder.reduce((result, line) => `${result}\n${line}`, ''); |
| 154 | + } |
| 155 | + }); |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // ? If this is the commit representing the earliest release (and there |
| 161 | + // ? are legacy releases), use this commit to report collected legacy |
| 162 | + // ? releases |
| 163 | + else { |
| 164 | + debug('::transform generating summary legacy release commit'); |
| 165 | + shouldGenerate = 'always'; |
| 166 | + |
| 167 | + const getShortHash = (h) => h.substring(0, 7); |
| 168 | + const shortHash = getShortHash(commit.hash); |
| 169 | + const url = context.repository |
| 170 | + ? `${context.host}/${context.owner}/${context.repository}` |
| 171 | + : context.repoUrl; |
| 172 | + |
| 173 | + const subject = legacyReleases |
| 174 | + .reverse() |
| 175 | + .map(({ hash, version }) => ({ |
| 176 | + url: `[${getShortHash(hash)}](${url}/commit/${hash})`, |
| 177 | + version |
| 178 | + })) |
| 179 | + .reduce( |
| 180 | + (subject, { url, version }) => `Version ${version} (${url})\n\n- ${subject}`, |
| 181 | + `Version ${commit.version}` |
| 182 | + ); |
| 183 | + |
| 184 | + commit = { |
| 185 | + type: null, |
| 186 | + scope: null, |
| 187 | + subject, |
| 188 | + id: null, |
| 189 | + source: null, |
| 190 | + merge: null, |
| 191 | + header: null, |
| 192 | + body: null, |
| 193 | + footer: null, |
| 194 | + notes: [], |
| 195 | + references: [], |
| 196 | + mentions: [], |
| 197 | + revert: null, |
| 198 | + hash: commit.hash, |
| 199 | + shortHash, |
| 200 | + gitTags: null, |
| 201 | + committerDate: 'pre-CI/CD', |
| 202 | + version: 'Archived Releases' |
| 203 | + }; |
| 204 | + } |
| 205 | + |
| 206 | + debug('::transform final commit = %O', commit); |
| 207 | + return commit; |
| 208 | + } |
| 209 | + } |
| 210 | +}; |
| 211 | + |
| 212 | +debug('exports = %O', module.exports); |
0 commit comments