@entro314labs/release-kit 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -6
- package/TRAIN.md +332 -0
- package/package.json +5 -2
- package/release.mjs +140 -71
- package/train.mjs +1241 -0
package/release.mjs
CHANGED
|
@@ -69,6 +69,12 @@ import { createInterface } from 'node:readline/promises'
|
|
|
69
69
|
* assets string[] files attached to the GitHub release
|
|
70
70
|
* notesFile string write the resolved release notes here, for a build tool that
|
|
71
71
|
* takes them as a file (goreleaser --release-notes, and similar)
|
|
72
|
+
* notes string where release notes come from: "auto" tries the changelog,
|
|
73
|
+
* then an assistant, then the commits; or force one of
|
|
74
|
+
* changelog / assistant / commits / github
|
|
75
|
+
* hiddenTypes string[] commit types to leave out of the notes; empty means report all
|
|
76
|
+
* ignoreCommits string[] regexes for commits that are bookkeeping rather than change:
|
|
77
|
+
* release commits, merges, work-in-progress, autosquash markers
|
|
72
78
|
* versioning string how `auto` derives a bump: "conventional", or
|
|
73
79
|
* always-patch / always-minor / always-major to never infer
|
|
74
80
|
* assistant string|object drafting CLI for commit messages and notes. A key of
|
|
@@ -94,8 +100,12 @@ import { createInterface } from 'node:readline/promises'
|
|
|
94
100
|
*/
|
|
95
101
|
const STEPS = ['commit', 'version', 'changelog', 'tag', 'push', 'publish', 'release']
|
|
96
102
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
103
|
+
/**
|
|
104
|
+
* All seven. `commit` is a conditional default: it no-ops on a clean tree, and on a dirty
|
|
105
|
+
* tree it proceeds only when a drafting assistant is configured — otherwise preflight
|
|
106
|
+
* still refuses the unclean tree. Opt out with `--skip commit` or a `steps` config.
|
|
107
|
+
*/
|
|
108
|
+
const DEFAULT_STEPS = [...STEPS]
|
|
99
109
|
|
|
100
110
|
const DEFAULTS = {
|
|
101
111
|
steps: DEFAULT_STEPS,
|
|
@@ -112,6 +122,14 @@ const DEFAULTS = {
|
|
|
112
122
|
assistant: null,
|
|
113
123
|
notesFile: null,
|
|
114
124
|
versioning: 'conventional',
|
|
125
|
+
notes: 'auto',
|
|
126
|
+
hiddenTypes: [],
|
|
127
|
+
ignoreCommits: [
|
|
128
|
+
'^chore\\(release\\)',
|
|
129
|
+
'^Merge (branch|pull request|remote)',
|
|
130
|
+
'^wip\\b',
|
|
131
|
+
'^(fixup|squash)!',
|
|
132
|
+
],
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
/**
|
|
@@ -146,13 +164,15 @@ Steps, in the fixed order they run. All but "commit" run by default:
|
|
|
146
164
|
Flags:
|
|
147
165
|
--only <steps> run only these steps, comma-separated
|
|
148
166
|
--skip <steps> run every step except these
|
|
149
|
-
--commit
|
|
167
|
+
--commit force the commit step on when a steps config removed it:
|
|
168
|
+
commit a dirty working tree with a
|
|
150
169
|
drafted Conventional Commits message instead of refusing to release
|
|
151
170
|
--preid <id> prerelease identifier (alpha, beta, rc, next, nightly, canary)
|
|
152
171
|
--dist-tag <name> override the npm dist-tag (default: derived from the version)
|
|
153
172
|
--dry-run print every step and execute nothing
|
|
154
173
|
--yes, -y skip the confirmation prompt
|
|
155
174
|
--notes-file <path> write the resolved release notes to a file for the next tool
|
|
175
|
+
--notes <source> where notes come from: auto, changelog, assistant, commits, github
|
|
156
176
|
--assistant <name> drafting CLI to use: auto, none, claude, codex
|
|
157
177
|
--assistant-model <name>
|
|
158
178
|
model the assistant runs with (e.g. sonnet, opus)
|
|
@@ -382,6 +402,7 @@ function runAssistant(prompt) {
|
|
|
382
402
|
|
|
383
403
|
/** Commit subjects since the last tag, with release and merge commits filtered out. */
|
|
384
404
|
function commitsSinceLastTag() {
|
|
405
|
+
const ignored = (config.ignoreCommits ?? []).map((pattern) => new RegExp(pattern, 'i'))
|
|
385
406
|
const lastTag = tryRead('git', ['describe', '--tags', '--abbrev=0'])
|
|
386
407
|
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD'
|
|
387
408
|
// %B is the whole message: bodies carry BREAKING CHANGE and Release-As footers. A record
|
|
@@ -397,22 +418,20 @@ function commitsSinceLastTag() {
|
|
|
397
418
|
const [subject, ...rest] = message.split('\n')
|
|
398
419
|
return { hash: hash.trim(), subject: subject.trim(), body: rest.join('\n').trim() }
|
|
399
420
|
})
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
!/^chore\(release\)/i.test(subject) &&
|
|
404
|
-
!/^Merge (branch|pull request|remote)/i.test(subject) &&
|
|
405
|
-
!/^wip\b/i.test(subject) &&
|
|
406
|
-
!/^(fixup|squash)!/.test(subject),
|
|
407
|
-
)
|
|
421
|
+
// Bookkeeping rather than change: the previous release's own commit, merges that
|
|
422
|
+
// duplicate the branch they bring in, and markers meant to be autosquashed away.
|
|
423
|
+
.filter(({ subject }) => subject && !ignored.some((re) => re.test(subject)))
|
|
408
424
|
const kept = withoutRevertedCommits(commits)
|
|
409
425
|
return { lastTag, commits: kept, subjects: kept.map((c) => c.subject) }
|
|
410
426
|
}
|
|
411
427
|
|
|
412
428
|
/**
|
|
413
|
-
* Conventional Commit types and the changelog heading each lands under
|
|
414
|
-
*
|
|
415
|
-
*
|
|
429
|
+
* Conventional Commit types and the changelog heading each lands under.
|
|
430
|
+
*
|
|
431
|
+
* Every type is reported. release-please and goreleaser hide `chore`, `ci`, `docs` and the
|
|
432
|
+
* rest by default, on the view that a reader upgrading does not care — but a changelog is a
|
|
433
|
+
* record, and silently omitting work makes it a partial one. A project that wants the
|
|
434
|
+
* shorter version lists the types to drop in `hiddenTypes`.
|
|
416
435
|
*/
|
|
417
436
|
const CHANGELOG_SECTIONS = [
|
|
418
437
|
{ type: 'feat', section: 'Features' },
|
|
@@ -420,13 +439,13 @@ const CHANGELOG_SECTIONS = [
|
|
|
420
439
|
{ type: 'perf', section: 'Performance Improvements' },
|
|
421
440
|
{ type: 'revert', section: 'Reverts' },
|
|
422
441
|
{ type: 'deps', section: 'Dependencies' },
|
|
423
|
-
{ type: 'docs', section: 'Documentation'
|
|
424
|
-
{ type: '
|
|
425
|
-
{ type: '
|
|
426
|
-
{ type: '
|
|
427
|
-
{ type: '
|
|
428
|
-
{ type: '
|
|
429
|
-
{ type: 'chore', section: 'Miscellaneous Chores'
|
|
442
|
+
{ type: 'docs', section: 'Documentation' },
|
|
443
|
+
{ type: 'refactor', section: 'Code Refactoring' },
|
|
444
|
+
{ type: 'build', section: 'Build System' },
|
|
445
|
+
{ type: 'ci', section: 'Continuous Integration' },
|
|
446
|
+
{ type: 'test', section: 'Tests' },
|
|
447
|
+
{ type: 'style', section: 'Styles' },
|
|
448
|
+
{ type: 'chore', section: 'Miscellaneous Chores' },
|
|
430
449
|
]
|
|
431
450
|
|
|
432
451
|
/**
|
|
@@ -498,7 +517,7 @@ function inferBump(commits, currentVersion, strategy = 'conventional') {
|
|
|
498
517
|
*
|
|
499
518
|
* @returns {string | null} markdown body, or null when nothing visible changed
|
|
500
519
|
*/
|
|
501
|
-
function changelogFromCommits(commits, links = null) {
|
|
520
|
+
function changelogFromCommits(commits, links = null, hidden = []) {
|
|
502
521
|
const parsed = commits.map((c) => parseCommit(c.subject, c.body, c.hash)).filter(Boolean)
|
|
503
522
|
const lines = []
|
|
504
523
|
|
|
@@ -523,8 +542,9 @@ function changelogFromCommits(commits, links = null) {
|
|
|
523
542
|
}
|
|
524
543
|
|
|
525
544
|
const known = new Set(CHANGELOG_SECTIONS.map((s) => s.type))
|
|
526
|
-
|
|
527
|
-
|
|
545
|
+
const hiddenTypes = new Set(hidden)
|
|
546
|
+
for (const { type, section } of CHANGELOG_SECTIONS) {
|
|
547
|
+
if (hiddenTypes.has(type)) continue
|
|
528
548
|
const inSection = parsed.filter(
|
|
529
549
|
(c) => c.type === type || (type === 'feat' && c.type === 'feature'),
|
|
530
550
|
)
|
|
@@ -537,7 +557,9 @@ function changelogFromCommits(commits, links = null) {
|
|
|
537
557
|
// A conventional type nobody anticipated — `security:`, `i18n:` — is still a change
|
|
538
558
|
// someone made deliberately. Dropping it silently is how a security fix goes unmentioned.
|
|
539
559
|
// The hidden types are excluded because hiding them is the point.
|
|
540
|
-
const other = parsed.filter(
|
|
560
|
+
const other = parsed.filter(
|
|
561
|
+
(c) => !known.has(c.type) && c.type !== 'feature' && !hiddenTypes.has(c.type),
|
|
562
|
+
)
|
|
541
563
|
if (other.length) {
|
|
542
564
|
lines.push('### Other Changes', '')
|
|
543
565
|
for (const c of other) lines.push(bullet(c, c.subject))
|
|
@@ -952,7 +974,7 @@ function rollUnreleased(text, version, date) {
|
|
|
952
974
|
|
|
953
975
|
// Remove the section wherever it sits, then place the release by version.
|
|
954
976
|
const withoutUnreleased = text.slice(0, match.index) + (next ? after.slice(next.index) : '')
|
|
955
|
-
const placed = insertChangelogSection(withoutUnreleased.trimEnd()
|
|
977
|
+
const placed = insertChangelogSection(`${withoutUnreleased.trimEnd()}\n`, version, date, body)
|
|
956
978
|
|
|
957
979
|
// A fresh [Unreleased] belongs above every release, whatever the file looked like before.
|
|
958
980
|
const first = sectionOffsets(placed)[0] ?? placed.length
|
|
@@ -1098,6 +1120,7 @@ const explicitDistTag = option('--dist-tag')
|
|
|
1098
1120
|
const requestedPreid = option('--preid')
|
|
1099
1121
|
const autoCommit = flag('--commit')
|
|
1100
1122
|
const requestedNotesFile = option('--notes-file')
|
|
1123
|
+
const requestedNotesSource = option('--notes')
|
|
1101
1124
|
const requestedAssistant = option('--assistant')
|
|
1102
1125
|
const requestedModel = option('--assistant-model')
|
|
1103
1126
|
const requestedEffort = option('--assistant-effort')
|
|
@@ -1166,23 +1189,33 @@ const VALUE_OPTIONS = new Set([
|
|
|
1166
1189
|
'--skip',
|
|
1167
1190
|
'--preid',
|
|
1168
1191
|
'--dist-tag',
|
|
1192
|
+
'--notes',
|
|
1169
1193
|
'--notes-file',
|
|
1170
1194
|
'--assistant',
|
|
1171
1195
|
'--assistant-model',
|
|
1172
1196
|
'--assistant-effort',
|
|
1173
1197
|
])
|
|
1174
1198
|
|
|
1175
|
-
/** The version or bump target: the
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
if (!argv[i].startsWith('-')) return argv[i]
|
|
1199
|
+
/** The version or bump target: the only argument that is neither a flag nor a flag's value. */
|
|
1200
|
+
const positionals = []
|
|
1201
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1202
|
+
if (VALUE_OPTIONS.has(argv[i])) {
|
|
1203
|
+
i += 1
|
|
1204
|
+
continue
|
|
1183
1205
|
}
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1206
|
+
if (!argv[i].startsWith('-')) positionals.push(argv[i])
|
|
1207
|
+
}
|
|
1208
|
+
const target = positionals[0]
|
|
1209
|
+
// A second positional is always a mistake, and silently ignoring it changes the release
|
|
1210
|
+
// that runs — `release-kit auto assistant auto` must not quietly mean `release-kit auto`.
|
|
1211
|
+
if (positionals.length > 1) {
|
|
1212
|
+
const extras = positionals.slice(1)
|
|
1213
|
+
const flagLike = extras.find((arg) => VALUE_OPTIONS.has(`--${arg}`))
|
|
1214
|
+
const hint = flagLike
|
|
1215
|
+
? `\n Flags are spelled with dashes: --${flagLike} ${extras[extras.indexOf(flagLike) + 1] ?? '<value>'}`
|
|
1216
|
+
: ''
|
|
1217
|
+
abort(`unexpected argument${extras.length > 1 ? 's' : ''}: ${extras.join(' ')}${hint}`)
|
|
1218
|
+
}
|
|
1186
1219
|
|
|
1187
1220
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1188
1221
|
// SETUP
|
|
@@ -1214,7 +1247,8 @@ if (unknownKeys.length) abort(`release.config.json has unknown keys: ${unknownKe
|
|
|
1214
1247
|
|
|
1215
1248
|
/**
|
|
1216
1249
|
* Which steps run: config provides the baseline, --only replaces it, --skip subtracts, and
|
|
1217
|
-
* --commit
|
|
1250
|
+
* --commit forces the step on when a steps config removed it. Unknown names are an
|
|
1251
|
+
* error rather than a silent no-op.
|
|
1218
1252
|
*/
|
|
1219
1253
|
const parseStepList = (value) =>
|
|
1220
1254
|
value
|
|
@@ -1539,7 +1573,7 @@ if (bumping && compareVersions(version, currentVersion) <= 0) {
|
|
|
1539
1573
|
|
|
1540
1574
|
const dirty = tryRead('git', ['status', '--porcelain'])
|
|
1541
1575
|
if (dirty === null) fail('could not read git status')
|
|
1542
|
-
else if (dirty && runs('commit')) {
|
|
1576
|
+
else if (dirty && runs('commit') && assistant) {
|
|
1543
1577
|
const entries = dirty.split('\n')
|
|
1544
1578
|
ok(`working tree has ${entries.length} change(s) — will be committed first`)
|
|
1545
1579
|
console.log(dim(indent(formatStatus(dirty))))
|
|
@@ -1558,16 +1592,17 @@ else if (dirty && runs('commit')) {
|
|
|
1558
1592
|
'releasing with --skip commit.',
|
|
1559
1593
|
)
|
|
1560
1594
|
}
|
|
1595
|
+
} else if (dirty && runs('commit')) {
|
|
1596
|
+
fail(
|
|
1597
|
+
`working tree is not clean:\n${indent(formatStatus(dirty))}\n` +
|
|
1598
|
+
' Configure a drafting assistant (`--assistant auto`, or "assistant" in ' +
|
|
1599
|
+
'release.config.json) to have these committed automatically, or commit them yourself.',
|
|
1600
|
+
)
|
|
1561
1601
|
} else if (dirty) {
|
|
1562
1602
|
fail(`working tree is not clean:\n${indent(formatStatus(dirty))}`)
|
|
1563
1603
|
} else ok('working tree clean')
|
|
1564
1604
|
|
|
1565
|
-
if (
|
|
1566
|
-
fail(
|
|
1567
|
-
'the commit step needs a drafting assistant to write the message. Configure one with ' +
|
|
1568
|
-
'`--assistant auto`, or set "assistant" in release.config.json.',
|
|
1569
|
-
)
|
|
1570
|
-
} else if (assistant) {
|
|
1605
|
+
if (assistant) {
|
|
1571
1606
|
const detail = [
|
|
1572
1607
|
assistantModel && `model ${assistantModel}`,
|
|
1573
1608
|
assistantEffort && `effort ${assistantEffort}`,
|
|
@@ -1731,7 +1766,23 @@ function reportChangelogOrder(text) {
|
|
|
1731
1766
|
}
|
|
1732
1767
|
}
|
|
1733
1768
|
|
|
1734
|
-
|
|
1769
|
+
/**
|
|
1770
|
+
* Where the notes come from. "auto" walks the list — a hand-written changelog section beats
|
|
1771
|
+
* anything generated — while an explicit source forces one, because asking for a thing and
|
|
1772
|
+
* being given something else is worse than being told the thing is unavailable.
|
|
1773
|
+
*/
|
|
1774
|
+
const NOTE_SOURCES = ['auto', 'changelog', 'assistant', 'commits', 'github']
|
|
1775
|
+
const notesSource = requestedNotesSource ?? config.notes ?? 'auto'
|
|
1776
|
+
if (!NOTE_SOURCES.includes(notesSource)) {
|
|
1777
|
+
abort(`unknown notes source "${notesSource}". Known: ${NOTE_SOURCES.join(', ')}`)
|
|
1778
|
+
}
|
|
1779
|
+
if (notesSource === 'assistant' && !assistant) {
|
|
1780
|
+
abort(
|
|
1781
|
+
'notes are set to come from an assistant, but none is available.\n' +
|
|
1782
|
+
' Add --assistant auto, or set "assistant" in release.config.json.',
|
|
1783
|
+
)
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1735
1786
|
let notes = null
|
|
1736
1787
|
let rolledChangelog = null
|
|
1737
1788
|
let draftedNotes = null
|
|
@@ -1761,7 +1812,9 @@ function draftNotesFor(v) {
|
|
|
1761
1812
|
'will not appear in the notes.',
|
|
1762
1813
|
)
|
|
1763
1814
|
}
|
|
1764
|
-
|
|
1815
|
+
// An explicitly named source wins over the assistant being merely available.
|
|
1816
|
+
if (!assistant || notesSource === 'commits')
|
|
1817
|
+
return changelogFromCommits(commits, remoteLinks(config.remote), config.hiddenTypes)
|
|
1765
1818
|
if (shallow) {
|
|
1766
1819
|
warn(
|
|
1767
1820
|
`shallow clone: only ${subjects.length} commit(s) are visible, so the notes will ` +
|
|
@@ -1771,42 +1824,58 @@ function draftNotesFor(v) {
|
|
|
1771
1824
|
note(`drafting notes from ${subjects.length} commit(s) with ${assistantName}...`)
|
|
1772
1825
|
return (
|
|
1773
1826
|
draftReleaseNotes(v, commits, lastTag, remoteLinks(config.remote)) ??
|
|
1774
|
-
changelogFromCommits(commits, remoteLinks(config.remote))
|
|
1827
|
+
changelogFromCommits(commits, remoteLinks(config.remote), config.hiddenTypes)
|
|
1775
1828
|
)
|
|
1776
1829
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1830
|
+
const changelogText =
|
|
1831
|
+
config.changelog && existsSync(config.changelog) ? readFileSync(config.changelog, 'utf8') : null
|
|
1832
|
+
if (changelogText) reportChangelogOrder(changelogText)
|
|
1833
|
+
|
|
1834
|
+
// `github` means write nothing and let GitHub generate from the commit log.
|
|
1835
|
+
if (notesSource === 'github') {
|
|
1836
|
+
note('notes will be generated by GitHub')
|
|
1837
|
+
} else if (notesSource === 'changelog' && !changelogText) {
|
|
1838
|
+
fail(`notes are set to come from ${config.changelog}, which does not exist`)
|
|
1839
|
+
} else {
|
|
1840
|
+
const wantsChangelog = notesSource === 'auto' || notesSource === 'changelog'
|
|
1841
|
+
|
|
1842
|
+
// A section already written for this version is the most authoritative thing there is.
|
|
1843
|
+
if (wantsChangelog && changelogText) {
|
|
1844
|
+
notes = changelogSection(changelogText, version)
|
|
1845
|
+
if (notes) ok(`${config.changelog} has a ${version} section`)
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Otherwise promote [Unreleased], which is equally hand-written.
|
|
1849
|
+
if (!notes && wantsChangelog && changelogText) {
|
|
1850
|
+
rolledChangelog = rollUnreleased(changelogText, version, new Date().toISOString().slice(0, 10))
|
|
1785
1851
|
if (rolledChangelog) {
|
|
1786
1852
|
notes = changelogSection(rolledChangelog, version)
|
|
1787
1853
|
ok(`${config.changelog}: [Unreleased] will become [${version}]`)
|
|
1788
|
-
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Generate, either because nothing was written or because a source was named.
|
|
1858
|
+
if (!notes) {
|
|
1859
|
+
if (notesDeferred) {
|
|
1860
|
+
ok('release notes will be drafted after the commit')
|
|
1789
1861
|
} else {
|
|
1790
|
-
draftedNotes =
|
|
1791
|
-
if (
|
|
1792
|
-
ok(`${config.changelog}: a ${version} section will be drafted after the commit`)
|
|
1793
|
-
} else if (draftedNotes) {
|
|
1862
|
+
draftedNotes = draftNotesFor(version)
|
|
1863
|
+
if (draftedNotes) {
|
|
1794
1864
|
notes = draftedNotes
|
|
1795
|
-
ok(
|
|
1796
|
-
|
|
1865
|
+
ok(
|
|
1866
|
+
notesSource === 'commits' || !assistant
|
|
1867
|
+
? 'release notes built from the commit log'
|
|
1868
|
+
: `release notes drafted by ${assistantName}`,
|
|
1869
|
+
)
|
|
1870
|
+
} else if (notesSource === 'auto') {
|
|
1797
1871
|
warn(
|
|
1798
|
-
|
|
1872
|
+
`no ${config.changelog ?? 'changelog'} section and nothing to draft from — GitHub will generate the notes`,
|
|
1799
1873
|
)
|
|
1874
|
+
} else {
|
|
1875
|
+
fail(`notes are set to come from "${notesSource}", which produced nothing`)
|
|
1800
1876
|
}
|
|
1801
1877
|
}
|
|
1802
1878
|
}
|
|
1803
|
-
} else {
|
|
1804
|
-
// No changelog file at all: there is nothing to roll, but notes can still be drafted for
|
|
1805
|
-
// the tag annotation and the GitHub release.
|
|
1806
|
-
notes = notesDeferred ? null : draftNotesFor(version)
|
|
1807
|
-
if (notesDeferred) ok('release notes will be drafted after the commit')
|
|
1808
|
-
else if (notes) ok(`release notes drafted by ${assistantName}`)
|
|
1809
|
-
else if (config.changelog) note(`no ${config.changelog} — GitHub will generate the notes`)
|
|
1810
1879
|
}
|
|
1811
1880
|
|
|
1812
1881
|
for (const asset of config.assets) {
|
|
@@ -1855,7 +1924,7 @@ if (dirty && runs('commit') && !dryRun) {
|
|
|
1855
1924
|
mutate('git', ['reset', '--quiet'])
|
|
1856
1925
|
abort(
|
|
1857
1926
|
`${assistantName} could not draft a Conventional Commits message for these changes.\n\n` +
|
|
1858
|
-
' Commit them yourself and re-run, or
|
|
1927
|
+
' Commit them yourself and re-run, or release with --skip commit.',
|
|
1859
1928
|
)
|
|
1860
1929
|
}
|
|
1861
1930
|
console.log(indent(commitMessage))
|