@entro314labs/release-kit 2.3.1 → 2.3.2

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 +1 -1
  2. package/release.mjs +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entro314labs/release-kit",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
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",
package/release.mjs CHANGED
@@ -756,9 +756,18 @@ function changelogSection(text, version) {
756
756
  * @returns {string | null} the updated document, or null when there is nothing to roll
757
757
  */
758
758
  function rollUnreleased(text, version, date) {
759
+ // A heading for this version already exists — possibly with an empty body, which
760
+ // `changelogSection` reports as absent. Rolling again would duplicate the heading.
761
+ if (new RegExp(`^##\\s+\\[?v?${escapeRe(version)}\\]?(?![\\w.-])`, 'm').test(text)) return null
762
+
759
763
  const heading = /^##\s+\[?Unreleased\]?[^\n]*$/im
760
764
  const match = heading.exec(text)
761
765
  if (!match) return null
766
+
767
+ // An empty [Unreleased] has nothing to promote; rolling it produces an empty section.
768
+ const rest = text.slice(match.index + match[0].length)
769
+ const body = (/^## /m.exec(rest) ? rest.slice(0, /^## /m.exec(rest).index) : rest).trim()
770
+ if (!body) return null
762
771
  const released = `## [Unreleased]\n\n## [${version}] - ${date}`
763
772
  return text.slice(0, match.index) + released + text.slice(match.index + match[0].length)
764
773
  }
@@ -1515,6 +1524,18 @@ for (const asset of config.assets) {
1515
1524
  else fail(`asset ${asset} does not exist`)
1516
1525
  }
1517
1526
 
1527
+ // Reusing a tag is the resume path, and a resume writes nothing. If this run would still
1528
+ // produce a commit, that commit moves HEAD past the tag and the release ends up tagged at
1529
+ // the wrong revision — which is silent until someone checks out the tag.
1530
+ if (taggedCommit && runs('tag') && (bumping || rolledChangelog)) {
1531
+ fail(
1532
+ `tag ${tag} already exists at HEAD, but this run would still commit ` +
1533
+ `${[bumping && 'a version bump', rolledChangelog && 'a changelog entry'].filter(Boolean).join(' and ')}.\n` +
1534
+ ' That commit would leave the tag behind HEAD. Release a new version, or use ' +
1535
+ '--only with the steps that remain.',
1536
+ )
1537
+ }
1538
+
1518
1539
  if (problems.length) {
1519
1540
  const summary = `${problems.length} preflight check(s) failed:\n - ${problems.join('\n - ')}`
1520
1541
  if (!dryRun) abort(summary)