@entro314labs/release-kit 2.9.1 → 2.9.3

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/release.mjs +45 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entro314labs/release-kit",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "Single-file, zero-dependency release mechanism for JS/TS/Node projects: version bump, changelog roll, commit, annotated tag, push, publish, GitHub release",
5
5
  "keywords": [
6
6
  "changelog",
@@ -49,8 +49,8 @@
49
49
  "release": "node release.mjs"
50
50
  },
51
51
  "devDependencies": {
52
- "oxfmt": "^0.63.0",
53
- "oxlint": "^1.78.0",
52
+ "oxfmt": "^0.65.0",
53
+ "oxlint": "^1.80.0",
54
54
  "semver": "^7.8.5"
55
55
  },
56
56
  "engines": {
package/release.mjs CHANGED
@@ -748,10 +748,19 @@ function changelogFromCommits(commits, links = null, hidden = [], contributors =
748
748
 
749
749
  const known = new Set(CHANGELOG_SECTIONS.map((s) => s.type))
750
750
  const hiddenTypes = new Set(hidden)
751
+ // A dependency bump is conventionally written `chore(deps):` or `build(deps):` — the scope
752
+ // carries the meaning, and it is what dependabot, renovate and this tool's own drafter
753
+ // emit, because `deps:` is not in @commitlint/config-conventional's type-enum. Without
754
+ // this those bumps sit in Miscellaneous Chores and Build System while the Dependencies
755
+ // section stays empty. `fix(deps):` is left where it is: a fix is still a bug fix.
756
+ const isDependency = (c) =>
757
+ c.type === 'deps' || (c.scope === 'deps' && (c.type === 'chore' || c.type === 'build'))
751
758
  for (const { type, section } of CHANGELOG_SECTIONS) {
752
759
  if (hiddenTypes.has(type)) continue
753
- const inSection = parsed.filter(
754
- (c) => c.type === type || (type === 'feat' && c.type === 'feature'),
760
+ const inSection = parsed.filter((c) =>
761
+ type === 'deps'
762
+ ? isDependency(c)
763
+ : !isDependency(c) && (c.type === type || (type === 'feat' && c.type === 'feature')),
755
764
  )
756
765
  if (!inSection.length) continue
757
766
  lines.push(`### ${section}`, '')
@@ -913,7 +922,24 @@ const CHANGELOG_TYPES = CHANGELOG_SECTIONS.map((s) => s.type)
913
922
 
914
923
  /** The drafter's types plus `feature`, the alias `changelogFromCommits` folds into feat. */
915
924
  const KNOWN_TYPES = new Set([...CHANGELOG_TYPES, 'feature'])
916
- const CONVENTIONAL_RE = new RegExp(`^(${[...KNOWN_TYPES].join('|')})(\\([^)]+\\))?!?: .+`)
925
+
926
+ /**
927
+ * The types this tool will write into somebody else's repository.
928
+ *
929
+ * Reading history is permissive on purpose — KNOWN_TYPES files whatever people wrote — but
930
+ * drafting is not, because a drafted subject has to survive the repository's own commit-msg
931
+ * hook. The near-universal gate is @commitlint/config-conventional, whose type-enum is
932
+ * exactly CHANGELOG_TYPES minus `deps`, and a drafted `deps: update dev tooling` aborted a
933
+ * release mid-commit with "type must be one of [build, chore, ci, ...]".
934
+ *
935
+ * Dependency work is still drafted and still reaches the Dependencies section: as
936
+ * `chore(deps):`, the scope dependabot and renovate emit, which `changelogFromCommits`
937
+ * routes there. Only the spelling the enum refuses is off the table.
938
+ */
939
+ const WRITE_TYPES = CHANGELOG_TYPES.filter((type) => type !== 'deps')
940
+
941
+ /** The gate on a drafted subject — WRITE_TYPES, not KNOWN_TYPES. See WRITE_TYPES. */
942
+ const CONVENTIONAL_RE = new RegExp(`^(${WRITE_TYPES.join('|')})(\\([^)]+\\))?!?: .+`)
917
943
 
918
944
  /**
919
945
  * Check commit subjects against the grammar the rest of this file reads.
@@ -971,7 +997,9 @@ function draftCommitMessage() {
971
997
  'Write a Conventional Commits message for these staged changes.',
972
998
  '',
973
999
  'Rules:',
974
- `- Subject: "<type>(<optional scope>): <description>" where type is one of ${CHANGELOG_TYPES.join(', ')}.`,
1000
+ `- Subject: "<type>(<optional scope>): <description>" where type is one of ${WRITE_TYPES.join(', ')}.`,
1001
+ '- For a dependency update use the `deps` scope — `chore(deps): ...` — which the',
1002
+ ' changelog files under Dependencies.',
975
1003
  '- Subject in the imperative mood, no trailing period, under 72 characters.',
976
1004
  '- Add a body only if the change needs explanation; separate it with a blank line.',
977
1005
  '- Output the raw commit message and nothing else: no markdown fences, no preamble.',
@@ -1377,16 +1405,26 @@ const sectionOffsets = (text) => [...text.matchAll(/^## .*$/gm)].map((m) => m.in
1377
1405
  */
1378
1406
  function insertChangelogSection(text, version, date, body) {
1379
1407
  const entry = `## [${version}] - ${date}\n\n${body}\n`
1380
- for (const offset of sectionOffsets(text)) {
1408
+ const offsets = sectionOffsets(text)
1409
+ let comparable = false
1410
+ for (const offset of offsets) {
1381
1411
  const heading = /^## \[?v?([\d.]+(?:-[\w.]+)?)\]?/m.exec(
1382
1412
  text.slice(offset, text.indexOf('\n', offset)),
1383
1413
  )
1384
- // An [Unreleased] heading has no version and always stays above the releases.
1385
- if (!heading) continue
1414
+ // Headings that carry no version — [Unreleased], and the date headings a changelog
1415
+ // that never adopted semver is made of — are not positions this can order against.
1416
+ if (!heading || !parseVersion(heading[1])) continue
1417
+ comparable = true
1386
1418
  if (compareVersions(version, heading[1]) > 0) {
1387
1419
  return `${text.slice(0, offset)}${entry}\n${text.slice(offset)}`
1388
1420
  }
1389
1421
  }
1422
+ // Falling out of the loop means every version section is newer, so the release belongs
1423
+ // at the foot. With nothing to compare against it belongs at the head instead: appending
1424
+ // to a date-headed changelog would file the release below its oldest entry.
1425
+ if (!comparable && offsets.length) {
1426
+ return `${text.slice(0, offsets[0])}${entry}\n${text.slice(offsets[0])}`
1427
+ }
1390
1428
  const trimmed = text.trimEnd()
1391
1429
  return `${trimmed}\n\n${entry}`
1392
1430
  }