relizy 1.7.0 → 1.9.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.
@@ -1,4 +1,4 @@
1
- import { logger, execPromise } from '@maz-ui/node';
1
+ import { execPromise, logger } from '@maz-ui/node';
2
2
  import { execSync, spawnSync } from 'node:child_process';
3
3
  import process$1 from 'node:process';
4
4
  import { formatJson } from '@maz-ui/utils/helpers/formatJson';
@@ -9,12 +9,120 @@ import { existsSync, readFileSync, statSync, writeFileSync, rmSync } from 'node:
9
9
  import path, { join, relative, sep } from 'node:path';
10
10
  import { select, confirm, input } from '@inquirer/prompts';
11
11
  import micromatch from 'micromatch';
12
+ import * as semver from 'semver';
12
13
  import { getErrorMessage } from '@maz-ui/utils/helpers/getErrorMessage';
13
14
  import fastGlob from 'fast-glob';
14
- import * as semver from 'semver';
15
15
  import { upperFirst } from '@maz-ui/utils/helpers/upperFirst';
16
16
  import { convert } from 'convert-gitmoji';
17
17
  import { fetch as fetch$1 } from 'node-fetch-native';
18
+ import { fromMarkdown } from 'mdast-util-from-markdown';
19
+ import { gfmFromMarkdown } from 'mdast-util-gfm';
20
+ import { gfm } from 'micromark-extension-gfm';
21
+
22
+ function shellSingleQuote(value) {
23
+ return `'${value.replaceAll("'", "'\\''")}'`;
24
+ }
25
+ async function isAncestor(ancestor, descendant, cwd) {
26
+ try {
27
+ await execPromise(
28
+ `git merge-base --is-ancestor ${shellSingleQuote(ancestor)} ${shellSingleQuote(descendant)}`,
29
+ { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
30
+ );
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function getCommitSubject(ref, cwd) {
37
+ try {
38
+ const { stdout } = await execPromise(
39
+ `git log -1 --format=%s ${shellSingleQuote(ref)}`,
40
+ { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
41
+ );
42
+ return stdout.trim() || null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ async function findReachableCommitBySubject(subject, to, cwd) {
48
+ try {
49
+ const { stdout } = await execPromise(
50
+ `git log ${shellSingleQuote(to)} --grep=${shellSingleQuote(subject)} --fixed-strings --format=%H -n 1`,
51
+ { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
52
+ );
53
+ return stdout.trim().split("\n")[0]?.trim() || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ async function tagExists(tag, cwd) {
59
+ try {
60
+ const tagRef = `refs/tags/${tag}`;
61
+ await execPromise(
62
+ `git rev-parse --verify --quiet ${shellSingleQuote(tagRef)}`,
63
+ { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
64
+ );
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ const LIST_TAGS_MAX_BUFFER = 64 * 1024 * 1024;
71
+ async function listTags({
72
+ pattern,
73
+ cwd,
74
+ logLevel
75
+ } = {}) {
76
+ const list = pattern ? ` --list ${shellSingleQuote(pattern)}` : "";
77
+ try {
78
+ const { stdout } = await execPromise(
79
+ `git tag${list} --sort=-creatordate`,
80
+ { cwd, logLevel, noStderr: true, noStdout: true, noSuccess: true, maxBuffer: LIST_TAGS_MAX_BUFFER }
81
+ );
82
+ return stdout.trim().split("\n").filter((tag) => tag.length > 0);
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+ async function retagAnnotatedLocal({
88
+ tag,
89
+ commit,
90
+ message,
91
+ signed,
92
+ cwd,
93
+ logLevel
94
+ }) {
95
+ const sign = signed ? "-s " : "";
96
+ await execPromise(
97
+ `git tag -f ${sign}-a ${shellSingleQuote(tag)} ${shellSingleQuote(commit)} -m ${shellSingleQuote(message)}`,
98
+ { cwd, logLevel, noStderr: true, noStdout: true }
99
+ );
100
+ }
101
+ async function pushTagForce(tag, cwd, logLevel) {
102
+ await execPromise(
103
+ `git push origin ${shellSingleQuote(tag)} --force`,
104
+ { cwd, logLevel, noStderr: true, noStdout: true }
105
+ );
106
+ }
107
+ const COMMIT_SEPARATOR = "";
108
+ const FIELD_SEPARATOR = "";
109
+ function getGitDiff(from, to = "HEAD", cwd) {
110
+ const range = from ? `${from}...${to}` : to;
111
+ const output = execSync(
112
+ `git --no-pager log "${range}" --pretty="%x1E%s%x1F%h%x1F%an%x1F%ae%n%b" --name-status`,
113
+ { cwd, encoding: "utf8" }
114
+ ).trim();
115
+ return output.split(COMMIT_SEPARATOR).slice(1).map((record) => {
116
+ const [firstLine = "", ...body] = record.split("\n");
117
+ const [message = "", shortHash = "", authorName = "", authorEmail = ""] = firstLine.split(FIELD_SEPARATOR);
118
+ return {
119
+ message,
120
+ shortHash,
121
+ author: { name: authorName, email: authorEmail },
122
+ body: body.join("\n")
123
+ };
124
+ });
125
+ }
18
126
 
19
127
  function getPackageDependencies({
20
128
  packagePath,
@@ -134,94 +242,6 @@ function topologicalSort(packages) {
134
242
  return sorted;
135
243
  }
136
244
 
137
- function shellSingleQuote(value) {
138
- return `'${value.replaceAll("'", "'\\''")}'`;
139
- }
140
- async function isAncestor(ancestor, descendant, cwd) {
141
- try {
142
- await execPromise(
143
- `git merge-base --is-ancestor ${shellSingleQuote(ancestor)} ${shellSingleQuote(descendant)}`,
144
- { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
145
- );
146
- return true;
147
- } catch {
148
- return false;
149
- }
150
- }
151
- async function getCommitSubject(ref, cwd) {
152
- try {
153
- const { stdout } = await execPromise(
154
- `git log -1 --format=%s ${shellSingleQuote(ref)}`,
155
- { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
156
- );
157
- return stdout.trim() || null;
158
- } catch {
159
- return null;
160
- }
161
- }
162
- async function findReachableCommitBySubject(subject, to, cwd) {
163
- try {
164
- const { stdout } = await execPromise(
165
- `git log ${shellSingleQuote(to)} --grep=${shellSingleQuote(subject)} --fixed-strings --format=%H -n 1`,
166
- { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
167
- );
168
- return stdout.trim().split("\n")[0]?.trim() || null;
169
- } catch {
170
- return null;
171
- }
172
- }
173
- async function tagExists(tag, cwd) {
174
- try {
175
- const tagRef = `refs/tags/${tag}`;
176
- await execPromise(
177
- `git rev-parse --verify --quiet ${shellSingleQuote(tagRef)}`,
178
- { cwd, noStderr: true, noStdout: true, noSuccess: true, noError: true }
179
- );
180
- return true;
181
- } catch {
182
- return false;
183
- }
184
- }
185
- async function retagAnnotatedLocal({
186
- tag,
187
- commit,
188
- message,
189
- signed,
190
- cwd,
191
- logLevel
192
- }) {
193
- const sign = signed ? "-s " : "";
194
- await execPromise(
195
- `git tag -f ${sign}-a ${shellSingleQuote(tag)} ${shellSingleQuote(commit)} -m ${shellSingleQuote(message)}`,
196
- { cwd, logLevel, noStderr: true, noStdout: true }
197
- );
198
- }
199
- async function pushTagForce(tag, cwd, logLevel) {
200
- await execPromise(
201
- `git push origin ${shellSingleQuote(tag)} --force`,
202
- { cwd, logLevel, noStderr: true, noStdout: true }
203
- );
204
- }
205
- const COMMIT_SEPARATOR = "";
206
- const FIELD_SEPARATOR = "";
207
- function getGitDiff(from, to = "HEAD", cwd) {
208
- const range = from ? `${from}...${to}` : to;
209
- const output = execSync(
210
- `git --no-pager log "${range}" --pretty="%x1E%s%x1F%h%x1F%an%x1F%ae%n%b" --name-status`,
211
- { cwd, encoding: "utf8" }
212
- ).trim();
213
- return output.split(COMMIT_SEPARATOR).slice(1).map((record) => {
214
- const [firstLine = "", ...body] = record.split("\n");
215
- const [message = "", shortHash = "", authorName = "", authorEmail = ""] = firstLine.split(FIELD_SEPARATOR);
216
- return {
217
- message,
218
- shortHash,
219
- author: { name: authorName, email: authorEmail },
220
- body: body.join("\n")
221
- };
222
- });
223
- }
224
-
225
245
  const sessionCache = /* @__PURE__ */ new Map();
226
246
  function resetRewrittenTagCache() {
227
247
  sessionCache.clear();
@@ -1104,7 +1124,7 @@ function updateLernaVersion({
1104
1124
  logger.fail(`Unable to update lerna.json: ${error}`);
1105
1125
  }
1106
1126
  }
1107
- function extractVersionFromPackageTag(tag) {
1127
+ function extractVersionFromPackageTag$1(tag) {
1108
1128
  const atIndex = tag.lastIndexOf("@");
1109
1129
  if (atIndex === -1) {
1110
1130
  return null;
@@ -1399,7 +1419,7 @@ function getIndependentTag({ version, name }) {
1399
1419
  }
1400
1420
  async function getLastStableTag({ logLevel, cwd }) {
1401
1421
  const { stdout } = await execPromise(
1402
- `git tag --sort=-creatordate | grep -E '^[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+$' | head -n 1`,
1422
+ `git tag --sort=-creatordate | grep -E '^[^0-9@]*[0-9]+\\.[0-9]+\\.[0-9]+$' | head -n 1`,
1403
1423
  {
1404
1424
  logLevel,
1405
1425
  noStderr: true,
@@ -1424,81 +1444,132 @@ async function getLastTag({ logLevel, cwd }) {
1424
1444
  logger.debug("Last tag:", lastTag || "No tags found");
1425
1445
  return lastTag;
1426
1446
  }
1427
- async function getAllRecentRepoTags(options) {
1428
- const limit = options?.limit;
1429
- try {
1430
- const { stdout } = await execPromise(
1431
- `git tag --sort=-creatordate | head -n ${limit}`,
1432
- {
1433
- logLevel: options?.logLevel,
1434
- noStderr: true,
1435
- noStdout: true,
1436
- noSuccess: true,
1437
- cwd: options?.cwd
1438
- }
1439
- );
1440
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
1441
- logger.debug(`Retrieved ${tags.length} recent repo tags`);
1442
- return tags;
1443
- } catch {
1444
- return [];
1447
+ const NEW_VERSION_PLACEHOLDER = "{{newVersion}}";
1448
+ function parseTagVersion(value) {
1449
+ return /^\d/.test(value) ? semver.valid(value) : null;
1450
+ }
1451
+ function splitTagBody(tagBody) {
1452
+ const index = tagBody.indexOf(NEW_VERSION_PLACEHOLDER);
1453
+ if (index === -1) {
1454
+ return null;
1455
+ }
1456
+ return {
1457
+ prefix: tagBody.slice(0, index),
1458
+ suffix: tagBody.slice(index + NEW_VERSION_PLACEHOLDER.length)
1459
+ };
1460
+ }
1461
+ function extractVersionFromRepoTag(tag, tagBody) {
1462
+ const parts = splitTagBody(tagBody);
1463
+ if (!parts || !tag.startsWith(parts.prefix) || !tag.endsWith(parts.suffix)) {
1464
+ return null;
1465
+ }
1466
+ return parseTagVersion(tag.slice(parts.prefix.length, tag.length - parts.suffix.length));
1467
+ }
1468
+ function extractVersionFromLegacyRepoTag(tag) {
1469
+ return parseTagVersion(tag.startsWith("v") ? tag.slice(1) : tag);
1470
+ }
1471
+ function extractVersionFromPackageTag(tag, packageName) {
1472
+ const prefix = `${packageName}@`;
1473
+ return tag.startsWith(prefix) ? parseTagVersion(tag.slice(prefix.length)) : null;
1474
+ }
1475
+ async function getRepoTags({
1476
+ tagBody,
1477
+ logLevel,
1478
+ cwd
1479
+ }) {
1480
+ const templateTags = [];
1481
+ const legacyTags = [];
1482
+ const unrecognizedTags = [];
1483
+ for (const tag of await listTags({ logLevel, cwd })) {
1484
+ const templateVersion = tagBody ? extractVersionFromRepoTag(tag, tagBody) : null;
1485
+ if (templateVersion) {
1486
+ templateTags.push({ tag, version: templateVersion });
1487
+ continue;
1488
+ }
1489
+ const legacyVersion = extractVersionFromLegacyRepoTag(tag);
1490
+ if (legacyVersion) {
1491
+ legacyTags.push({ tag, version: legacyVersion });
1492
+ } else if (!tag.includes("@") && tag.split(/[^\d.]/).some((part) => semver.valid(part))) {
1493
+ unrecognizedTags.push(tag);
1494
+ }
1445
1495
  }
1496
+ logger.debug(`Retrieved ${templateTags.length} repo tags following "${tagBody}" and ${legacyTags.length} in the v1.2.3 or 1.2.3 format`);
1497
+ return { tags: [...templateTags, ...legacyTags], unrecognizedTags };
1446
1498
  }
1447
- async function getAllRecentPackageTags({
1499
+ async function getPackageTags({
1448
1500
  packageName,
1449
- limit = 50,
1450
1501
  logLevel,
1451
1502
  cwd
1452
1503
  }) {
1453
- try {
1454
- const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
1455
- const { stdout } = await execPromise(
1456
- `git tag --sort=-creatordate | grep -E '^${escapedPackageName}@' | head -n ${limit}`,
1457
- {
1458
- logLevel,
1459
- noStderr: true,
1460
- noStdout: true,
1461
- noSuccess: true,
1462
- cwd
1463
- }
1464
- );
1465
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
1466
- logger.debug(`Retrieved ${tags.length} recent tags for package ${packageName}`);
1467
- return tags;
1468
- } catch {
1469
- return [];
1504
+ const tags = [];
1505
+ for (const tag of await listTags({ pattern: `${packageName}@*`, logLevel, cwd })) {
1506
+ const version = extractVersionFromPackageTag(tag, packageName);
1507
+ if (version) {
1508
+ tags.push({ tag, version });
1509
+ }
1470
1510
  }
1511
+ logger.debug(`Retrieved ${tags.length} tags for package ${packageName}`);
1512
+ return tags;
1471
1513
  }
1472
- function filterCompatibleTags({
1514
+ function selectPreviousTag({
1473
1515
  tags,
1474
- pkg,
1475
- onlyStable
1516
+ currentVersion,
1517
+ maxVersion,
1518
+ onlyStable,
1519
+ label
1476
1520
  }) {
1477
- const filtered = tags.filter((tag) => {
1478
- const tagVersion = extractVersionFromTag(tag, pkg.name);
1479
- if (!tagVersion) {
1480
- logger.debug(`Skipping tag ${tag}: cannot extract version`);
1481
- return false;
1482
- }
1483
- if (onlyStable && isPrerelease(tagVersion)) {
1484
- logger.debug(`Skipping tag ${tag}: prerelease version ${tagVersion} (onlyStable=${onlyStable})`);
1521
+ const candidates = tags.filter(({ tag, version }) => {
1522
+ if (!isTagVersionCompatibleWithCurrent(version, currentVersion)) {
1523
+ logger.debug(`Skipping tag ${tag}: version ${version} has higher major than current ${currentVersion}`);
1485
1524
  return false;
1486
1525
  }
1487
- if (!isTagVersionCompatibleWithCurrent(tagVersion, pkg.version)) {
1488
- logger.debug(`Skipping tag ${tag}: version ${tagVersion} has higher major than current ${pkg.version}`);
1526
+ if (semver.valid(maxVersion) && semver.gt(version, maxVersion)) {
1527
+ logger.debug(`Skipping tag ${tag}: version ${version} is higher than ${maxVersion}`);
1489
1528
  return false;
1490
1529
  }
1491
- logger.debug(`Tag ${tag} with version ${tagVersion} is compatible`);
1492
1530
  return true;
1493
1531
  });
1494
- logger.debug(`Filtered ${tags.length} tags down to ${filtered.length} compatible tags`);
1495
- return filtered;
1532
+ const highest = (list) => list.reduce(
1533
+ (best, candidate) => !best || semver.gt(candidate.version, best.version) ? candidate : best,
1534
+ null
1535
+ );
1536
+ if (onlyStable) {
1537
+ const stable = highest(candidates.filter(({ version }) => !isPrerelease(version)));
1538
+ if (stable) {
1539
+ logger.debug(`Previous ${label} tag: ${stable.tag}`);
1540
+ return stable.tag;
1541
+ }
1542
+ }
1543
+ const previous = highest(candidates);
1544
+ if (previous) {
1545
+ if (onlyStable) {
1546
+ logger.info(`No stable ${label} tag found for version ${currentVersion}, falling back to last prerelease tag: ${previous.tag}`);
1547
+ } else {
1548
+ logger.debug(`Previous ${label} tag: ${previous.tag}`);
1549
+ }
1550
+ return previous.tag;
1551
+ }
1552
+ logger.info(`No compatible ${label} tag found for version ${currentVersion}`);
1553
+ return null;
1554
+ }
1555
+ const warnedUnrecognizedTags = /* @__PURE__ */ new Set();
1556
+ function warnUnrecognizedTags({ tags, tagBody, cwd }) {
1557
+ const key = `${cwd}:${tagBody}`;
1558
+ if (tags.length === 0 || warnedUnrecognizedTags.has(key)) {
1559
+ return;
1560
+ }
1561
+ warnedUnrecognizedTags.add(key);
1562
+ logger.warn(
1563
+ `No previous release tag follows templates.tagBody "${tagBody}", but some tags look like one (${tags.slice(0, 3).join(", ")}). If the template was changed, pass the tag of the previous release with --from.`
1564
+ );
1496
1565
  }
1497
1566
  function getLastRepoTag(options) {
1498
1567
  if (options?.pkg) {
1499
1568
  return getLastRepoTagWithFiltering({
1500
1569
  pkg: options.pkg,
1501
1570
  onlyStable: options.onlyStable ?? false,
1571
+ tagBody: options.tagBody,
1572
+ maxVersion: options.currentVersion ?? options.pkg.version,
1502
1573
  logLevel: options.logLevel,
1503
1574
  cwd: options.cwd
1504
1575
  });
@@ -1511,40 +1582,24 @@ function getLastRepoTag(options) {
1511
1582
  async function getLastRepoTagWithFiltering({
1512
1583
  pkg,
1513
1584
  onlyStable,
1585
+ tagBody,
1586
+ maxVersion,
1514
1587
  logLevel,
1515
1588
  cwd
1516
1589
  }) {
1517
- const recentTags = await getAllRecentRepoTags({ limit: 50, logLevel, cwd });
1518
- if (recentTags.length === 0) {
1519
- logger.info("No tags found in repository");
1590
+ const { tags, unrecognizedTags } = await getRepoTags({ tagBody, logLevel, cwd });
1591
+ if (tags.length === 0) {
1592
+ logger.info("No release tags found in repository");
1593
+ warnUnrecognizedTags({ tags: unrecognizedTags, tagBody, cwd });
1520
1594
  return null;
1521
1595
  }
1522
- const compatibleTags = filterCompatibleTags({
1523
- tags: recentTags,
1524
- pkg,
1525
- onlyStable
1596
+ return selectPreviousTag({
1597
+ tags,
1598
+ currentVersion: pkg.version,
1599
+ maxVersion,
1600
+ onlyStable,
1601
+ label: "repo"
1526
1602
  });
1527
- if (compatibleTags.length > 0) {
1528
- const lastTag = compatibleTags[0];
1529
- logger.debug(`Last compatible repo tag: ${lastTag}`);
1530
- return lastTag;
1531
- }
1532
- if (onlyStable) {
1533
- const prereleaseFallback = filterCompatibleTags({
1534
- tags: recentTags,
1535
- pkg,
1536
- onlyStable: false
1537
- });
1538
- if (prereleaseFallback.length > 0) {
1539
- const fallbackTag = prereleaseFallback[0];
1540
- logger.info(
1541
- `No stable repo tag found for version ${pkg.version}, falling back to last prerelease tag: ${fallbackTag}`
1542
- );
1543
- return fallbackTag;
1544
- }
1545
- }
1546
- logger.info(`No compatible tags found for version ${pkg.version}`);
1547
- return null;
1548
1603
  }
1549
1604
  async function getLastPackageTag({
1550
1605
  pkg,
@@ -1590,42 +1645,18 @@ async function getLastPackageTagWithFiltering({
1590
1645
  logLevel,
1591
1646
  cwd
1592
1647
  }) {
1593
- const recentTags = await getAllRecentPackageTags({
1594
- packageName: pkg.name,
1595
- limit: 50,
1596
- logLevel,
1597
- cwd
1598
- });
1599
- if (recentTags.length === 0) {
1648
+ const tags = await getPackageTags({ packageName: pkg.name, logLevel, cwd });
1649
+ if (tags.length === 0) {
1600
1650
  logger.info(`No tags found for package ${pkg.name}`);
1601
1651
  return null;
1602
1652
  }
1603
- const compatibleTags = filterCompatibleTags({
1604
- tags: recentTags,
1605
- pkg,
1606
- onlyStable
1653
+ return selectPreviousTag({
1654
+ tags,
1655
+ currentVersion: pkg.version,
1656
+ maxVersion: pkg.version,
1657
+ onlyStable,
1658
+ label: `${pkg.name} package`
1607
1659
  });
1608
- if (compatibleTags.length > 0) {
1609
- const lastTag = compatibleTags[0];
1610
- logger.debug(`Last compatible package tag for ${pkg.name}: ${lastTag}`);
1611
- return lastTag;
1612
- }
1613
- if (onlyStable) {
1614
- const prereleaseFallback = filterCompatibleTags({
1615
- tags: recentTags,
1616
- pkg,
1617
- onlyStable: false
1618
- });
1619
- if (prereleaseFallback.length > 0) {
1620
- const fallbackTag = prereleaseFallback[0];
1621
- logger.info(
1622
- `No stable tags found for package ${pkg.name} with version ${pkg.version}, falling back to last prerelease tag: ${fallbackTag}`
1623
- );
1624
- return fallbackTag;
1625
- }
1626
- }
1627
- logger.info(`No compatible tags found for package ${pkg.name} with version ${pkg.version}`);
1628
- return null;
1629
1660
  }
1630
1661
  const NEW_PACKAGE_MARKER = "__NEW_PACKAGE__";
1631
1662
  async function resolveFromTagIndependent({
@@ -1648,6 +1679,18 @@ async function resolveFromTagIndependent({
1648
1679
  }
1649
1680
  return lastPackageTag;
1650
1681
  }
1682
+ function getRepoCurrentVersion({ cwd, pkg }) {
1683
+ let rootVersion = null;
1684
+ try {
1685
+ rootVersion = semver.valid(JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")).version);
1686
+ } catch {
1687
+ logger.debug(`Cannot read the root version in ${cwd}, using the version of ${pkg.name}`);
1688
+ }
1689
+ if (!rootVersion || !semver.valid(pkg.version)) {
1690
+ return rootVersion || pkg.version;
1691
+ }
1692
+ return semver.gt(rootVersion, pkg.version) ? rootVersion : pkg.version;
1693
+ }
1651
1694
  async function resolveFromTagUnified({
1652
1695
  config,
1653
1696
  pkg,
@@ -1659,6 +1702,8 @@ async function resolveFromTagUnified({
1659
1702
  const lastRepoTag = await getLastRepoTag({
1660
1703
  pkg,
1661
1704
  onlyStable,
1705
+ tagBody: config.templates.tagBody,
1706
+ currentVersion: getRepoCurrentVersion({ cwd: config.cwd, pkg }),
1662
1707
  logLevel,
1663
1708
  cwd: config.cwd
1664
1709
  });
@@ -2536,7 +2581,8 @@ const SENSITIVE_KEYS = /* @__PURE__ */ new Set([
2536
2581
  "_authtoken"
2537
2582
  ]);
2538
2583
  const SECRET_CONTAINER_KEYS = /* @__PURE__ */ new Set(["tokens"]);
2539
- function redactValue(value, inSecretContainer, keyIsSensitive) {
2584
+ const CIRCULAR = "[Circular]";
2585
+ function redactValue(value, inSecretContainer, keyIsSensitive, ancestors) {
2540
2586
  if (typeof value === "string") {
2541
2587
  if (value === "") {
2542
2588
  return value;
@@ -2546,26 +2592,29 @@ function redactValue(value, inSecretContainer, keyIsSensitive) {
2546
2592
  }
2547
2593
  return value;
2548
2594
  }
2549
- if (Array.isArray(value)) {
2550
- return value.map((item) => redactValue(item, inSecretContainer, false));
2595
+ if (!value || typeof value !== "object") {
2596
+ return value;
2551
2597
  }
2552
- if (value && typeof value === "object") {
2553
- return redactObject(value, inSecretContainer);
2598
+ if (ancestors.has(value)) {
2599
+ return CIRCULAR;
2554
2600
  }
2555
- return value;
2601
+ ancestors.add(value);
2602
+ const result = Array.isArray(value) ? value.map((item) => redactValue(item, inSecretContainer, false, ancestors)) : redactObject(value, inSecretContainer, ancestors);
2603
+ ancestors.delete(value);
2604
+ return result;
2556
2605
  }
2557
- function redactObject(obj, inSecretContainer) {
2606
+ function redactObject(obj, inSecretContainer, ancestors) {
2558
2607
  const result = {};
2559
2608
  for (const [key, value] of Object.entries(obj)) {
2560
2609
  const lowerKey = key.toLowerCase();
2561
2610
  const childInContainer = inSecretContainer || SECRET_CONTAINER_KEYS.has(lowerKey);
2562
2611
  const keyIsSensitive = SENSITIVE_KEYS.has(lowerKey);
2563
- result[key] = redactValue(value, childInContainer, keyIsSensitive);
2612
+ result[key] = redactValue(value, childInContainer, keyIsSensitive, ancestors);
2564
2613
  }
2565
2614
  return result;
2566
2615
  }
2567
2616
  function redactSecrets(value) {
2568
- return redactValue(value, false, false);
2617
+ return redactValue(value, false, false, /* @__PURE__ */ new WeakSet());
2569
2618
  }
2570
2619
 
2571
2620
  function getDefaultConfig() {
@@ -2667,7 +2716,7 @@ function getDefaultConfig() {
2667
2716
  slack: {
2668
2717
  enabled: false,
2669
2718
  onlyStable: true,
2670
- postMaxLength: 2500,
2719
+ changelogFormat: "summary",
2671
2720
  noAuthors: false,
2672
2721
  noPackages: false
2673
2722
  }
@@ -3416,15 +3465,19 @@ function getCommitBody(commit) {
3416
3465
  const contentLines = lines.filter((line) => {
3417
3466
  const trimmedLine = line.trim();
3418
3467
  if (!trimmedLine) {
3419
- return false;
3468
+ return true;
3420
3469
  }
3421
3470
  const isFileLine = /^[AMDTUXB](?:\d{3})?\s+/.test(trimmedLine) || /^[RCM]\d{3}\s+/.test(trimmedLine);
3422
3471
  return !isFileLine;
3423
3472
  });
3424
- if (contentLines.length === 0) {
3473
+ const paragraphLines = contentLines.filter((line, index) => line.trim() || contentLines[index - 1]?.trim());
3474
+ while (paragraphLines.length > 0 && !paragraphLines.at(-1)?.trim()) {
3475
+ paragraphLines.pop();
3476
+ }
3477
+ if (paragraphLines.length === 0) {
3425
3478
  return "";
3426
3479
  }
3427
- const indentedBody = contentLines.map((line) => ` ${line}`).join("\n");
3480
+ const indentedBody = paragraphLines.map((line) => line.trim() ? ` ${line}` : "").join("\n");
3428
3481
  return `
3429
3482
 
3430
3483
  ${indentedBody}
@@ -3633,29 +3686,76 @@ ${existingChangelog}`;
3633
3686
  const BASE_PROMPT = `You are a release-notes rewriter.
3634
3687
  The user message contains a markdown changelog built from conventional commits, wrapped in a <changelog> tag. Your job: rewrite the content inside that tag.
3635
3688
  Never ask for clarification. Never reply with questions, greetings, or meta-commentary. Your only output is the rewritten changelog content.
3636
- Never invent changes that are not in the input \u2014 if something is not mentioned, it does not exist.
3637
- Never include compare links, contributor lists, or release metadata \u2014 only the content provided.
3638
- Respond with the rewritten content only \u2014 no preamble, no explanation, no surrounding code fence, no <changelog> tag.
3689
+ Never invent changes: if something is not mentioned in the input, it does not exist.
3690
+ Text indented under a bullet is the body of that commit: use it to understand and describe the change, never treat it as a separate change.
3691
+ If the input has one section per package (headings such as "## my-package@1.2.0"), keep each change attributed to its package.
3692
+ Never include compare links, contributor lists, or release metadata: only the content provided.
3693
+ Respond with the rewritten content only: no preamble, no explanation, no surrounding code fence, no <changelog> tag.
3639
3694
  Output language: {{language}}.`;
3640
3695
  const PROVIDER_RELEASE_PROMPT = `Format the output as markdown with "### <Type>" sections matching the input (Features, Bug Fixes, etc.).
3641
3696
  Merge redundant items that describe the same change.
3642
- Rewrite each bullet for end-user clarity \u2014 focus on what changed for the user, not internal details.
3697
+ Rewrite each bullet for end-user clarity: focus on what changed for the user, not internal details.
3643
3698
  Preserve exactly as given: PR and issue references like #123, commit hashes, commit scopes like **auth:**, and all markdown links.
3644
3699
  Preserve the \u26A0\uFE0F marker on breaking items and the "#### \u26A0\uFE0F Breaking Changes" section if present.
3645
3700
  Purely internal commits (chore, refactor with no user-visible impact) may be dropped unless they carry meaningful information.
3646
3701
  Tone: professional, concise, public-facing release notes.`;
3647
- const TWITTER_PROMPT = `Output plain text \u2014 no markdown. A leading "#" becomes a hashtag on Twitter, so avoid it.
3648
- Hard maximum: {{maxLength}} characters \u2014 never exceed it.
3649
- Remove commit hashes (e.g. "aabf96b") and PR/issue references (e.g. "#123") \u2014 they are meaningless on Twitter.
3650
- If the input has one change, write one substantive sentence about it (ground it in the input, never invent).
3651
- If the input has multiple changes, surface 2 to 4 highlights.
3652
- You produce ONLY the changelog content \u2014 the outer template will add the project name, version, and URLs around it.
3653
- Tone: enthusiastic but not cringy. At most 1-2 emojis total.
3654
- Do not add hashtags unless the user explicitly supplies them.`;
3655
- const SLACK_PROMPT = `Format with Slack-compatible markdown: *bold*, _italic_, \`code\`, and "-" bullet lists.
3656
- Keep it concise \u2014 3 to 8 bullets maximum.
3657
- If any breaking changes are present, lead with them.
3702
+ const TWITTER_PROMPT = `Output plain text: no markdown syntax at all (no "**", "_", backticks, headings, bullets or links).
3703
+ Never start a word with "#": Twitter turns it into a hashtag. Only add hashtags when the extra guidelines ask for them.
3704
+ Remove commit hashes (e.g. "aabf96b"), PR and issue references (e.g. "#123") and scope markers (e.g. "**auth:**"): mention the scope in the sentence only when it helps.
3705
+ If breaking changes are present, mention them first.
3706
+ If the input has one change, write one substantive sentence about it.
3707
+ If the input has multiple changes, surface the 2 to 4 most user-visible ones.
3708
+ You produce ONLY the changelog content: the outer template adds the project name, version, and URLs around it.
3709
+ Tone: enthusiastic but not cringy. At most 1 or 2 emojis in total.`;
3710
+ const SLACK_PROMPT = `Format the output as standard markdown: "### <Type>" headings matching the input, "- " bullets, **bold**, _italic_ and \`code\`.
3711
+ Never use Slack mrkdwn syntax (single-asterisk bold, <url|text> links): the output is converted for Slack automatically.
3712
+ Keep PR and issue references as the markdown links given in the input (e.g. "([#123](https://...))").
3713
+ If the input has package headings, keep them as "## " headings.
3714
+ If breaking changes are present, lead with them.
3715
+ Keep it scannable: 3 to 8 bullets in total, one line each, focused on what changed for the team.
3658
3716
  Tone: factual, oriented toward an internal team audience.`;
3717
+ const MAX_LENGTH_PROMPT = `Hard maximum: {{maxLength}} characters. Never exceed it.`;
3718
+ function wrapChangelogPrompt(prompt) {
3719
+ return `<changelog>
3720
+ ${prompt}
3721
+ </changelog>
3722
+
3723
+ Rewrite the content inside the <changelog> tag per the rules in the system prompt. Output ONLY the rewritten content, with no preamble, no explanation, no surrounding tags.`;
3724
+ }
3725
+
3726
+ function getModel(config) {
3727
+ const model = config.ai?.providers?.["ai-sdk"]?.model;
3728
+ if (!model) {
3729
+ throw new Error(
3730
+ "No model configured for the ai-sdk provider. Set ai.providers['ai-sdk'].model, e.g. openai('gpt-5-mini') from @ai-sdk/openai."
3731
+ );
3732
+ }
3733
+ return model;
3734
+ }
3735
+ const aiSdkProvider = {
3736
+ name: "ai-sdk",
3737
+ async safetyCheck(config) {
3738
+ getModel(config);
3739
+ try {
3740
+ await import('ai');
3741
+ } catch {
3742
+ throw new Error("ai is not installed. Install it with: pnpm add -D ai");
3743
+ }
3744
+ },
3745
+ async generate(config, request) {
3746
+ const model = getModel(config);
3747
+ const { generateText } = await import('ai');
3748
+ const reasoning = config.ai?.providers?.["ai-sdk"]?.reasoning;
3749
+ const result = await generateText({
3750
+ model,
3751
+ instructions: request.systemPrompt,
3752
+ prompt: wrapChangelogPrompt(request.prompt),
3753
+ ...reasoning && { reasoning }
3754
+ });
3755
+ logger.verbose("AI SDK steps:", result.steps?.flatMap((step) => step.content.map((part) => part.type)).join(", "));
3756
+ return result.text.trim();
3757
+ }
3758
+ };
3659
3759
 
3660
3760
  function claudeBinaryAvailable() {
3661
3761
  try {
@@ -3699,16 +3799,11 @@ const claudeCodeProvider = {
3699
3799
  const { claudeRun } = await import('@yoloship/claude-sdk');
3700
3800
  const auth = resolveAuth(config);
3701
3801
  const model = config.ai?.providers?.["claude-code"]?.model;
3702
- const wrappedPrompt = `<changelog>
3703
- ${request.prompt}
3704
- </changelog>
3705
-
3706
- Rewrite the content inside the <changelog> tag per the rules in the system prompt. Output ONLY the rewritten content, with no preamble, no explanation, no surrounding tags.`;
3707
3802
  const hasAuth = !!(auth.apiKey || auth.oauthToken);
3708
3803
  const result = await claudeRun(
3709
3804
  {
3710
3805
  systemPrompt: request.systemPrompt,
3711
- prompt: wrappedPrompt,
3806
+ prompt: wrapChangelogPrompt(request.prompt),
3712
3807
  maxTurns: 1,
3713
3808
  effort: "low",
3714
3809
  disableSlashCommands: true,
@@ -3725,7 +3820,8 @@ Rewrite the content inside the <changelog> tag per the rules in the system promp
3725
3820
  };
3726
3821
 
3727
3822
  const providers = {
3728
- "claude-code": claudeCodeProvider
3823
+ "claude-code": claudeCodeProvider,
3824
+ "ai-sdk": aiSdkProvider
3729
3825
  };
3730
3826
  function getAIProvider(config) {
3731
3827
  const name = config.ai?.provider ?? "claude-code";
@@ -3751,6 +3847,16 @@ function substitutePlaceholders(prompt, vars) {
3751
3847
  }
3752
3848
  return result;
3753
3849
  }
3850
+ function getExtraGuidelines(config, target) {
3851
+ const guidelines = config.ai?.extraGuidelines;
3852
+ if (!guidelines) {
3853
+ return [];
3854
+ }
3855
+ if (typeof guidelines === "string") {
3856
+ return [guidelines];
3857
+ }
3858
+ return [guidelines.global, guidelines[target]].filter((guideline) => !!guideline?.trim());
3859
+ }
3754
3860
  function assemblePrompt(config, target, maxLength) {
3755
3861
  const vars = {
3756
3862
  language: config.ai?.language ?? "en",
@@ -3761,9 +3867,10 @@ function assemblePrompt(config, target, maxLength) {
3761
3867
  return substitutePlaceholders(override, vars);
3762
3868
  }
3763
3869
  const parts = [BASE_PROMPT, PLATFORM_PROMPTS[target]];
3764
- if (config.ai?.extraGuidelines) {
3765
- parts.push(config.ai.extraGuidelines);
3870
+ if (maxLength !== void 0) {
3871
+ parts.push(MAX_LENGTH_PROMPT);
3766
3872
  }
3873
+ parts.push(...getExtraGuidelines(config, target));
3767
3874
  return substitutePlaceholders(parts.join("\n\n"), vars);
3768
3875
  }
3769
3876
  function applyAIOverride(config, ai) {
@@ -4705,6 +4812,648 @@ async function postPrComment({
4705
4812
  }
4706
4813
  }
4707
4814
 
4815
+ const SLACK_MAX_BLOCKS = 50;
4816
+ const SLACK_SECTION_MAX_LENGTH = 3e3;
4817
+ const SLACK_RICH_TEXT_BLOCK_MAX_LENGTH = 3e3;
4818
+ const SLACK_RICH_TEXT_MESSAGE_MAX_LENGTH = 11e3;
4819
+ const SLACK_HEADER_MAX_LENGTH = 150;
4820
+ const ELLIPSIS = "\u2026";
4821
+ const SLACK_MAX_LIST_INDENT = 8;
4822
+ function parseMarkdown(markdown) {
4823
+ return fromMarkdown(markdown, {
4824
+ extensions: [gfm()],
4825
+ mdastExtensions: [gfmFromMarkdown()]
4826
+ });
4827
+ }
4828
+ function splitIntoUnits(root) {
4829
+ const units = [];
4830
+ for (const node of root.children) {
4831
+ if (node.type === "heading") {
4832
+ units.push({ type: "heading", node });
4833
+ } else if (node.type === "list") {
4834
+ node.children.forEach((item, index) => units.push({ type: "item", node: item, list: node, index }));
4835
+ } else {
4836
+ units.push({ type: "block", node });
4837
+ }
4838
+ }
4839
+ return units;
4840
+ }
4841
+ function countOmittedChanges(units) {
4842
+ const items = units.filter((unit) => unit.type === "item").length;
4843
+ return items > 0 ? items : units.filter((unit) => unit.type !== "heading").length;
4844
+ }
4845
+ function formatOmittedChanges(count) {
4846
+ return count > 0 ? `\u2026 and ${count} more change${count > 1 ? "s" : ""}` : "";
4847
+ }
4848
+ function sliceSlackText(text, length) {
4849
+ const end = Math.max(0, length);
4850
+ const lastCode = text.charCodeAt(end - 1);
4851
+ return text.slice(0, lastCode >= 55296 && lastCode <= 56319 ? end - 1 : end);
4852
+ }
4853
+ function dropTrailingHeadings(rendered) {
4854
+ let end = rendered.length;
4855
+ while (end > 0 && rendered[end - 1]?.unit.type === "heading") {
4856
+ end--;
4857
+ }
4858
+ return rendered.slice(0, end);
4859
+ }
4860
+ function getListItemParts(item) {
4861
+ const [first, ...rest] = item.children;
4862
+ if (first?.type === "paragraph") {
4863
+ return { head: first.children, body: rest };
4864
+ }
4865
+ return { head: [], body: item.children };
4866
+ }
4867
+ function getCheckboxPrefix(item) {
4868
+ if (item.checked === true) {
4869
+ return "\u2611 ";
4870
+ }
4871
+ if (item.checked === false) {
4872
+ return "\u2610 ";
4873
+ }
4874
+ return "";
4875
+ }
4876
+ function getListItemNumber(list, index) {
4877
+ return (list.start ?? 1) + index;
4878
+ }
4879
+ function getAltText(node) {
4880
+ return node.alt ?? "";
4881
+ }
4882
+ function toPlainText(node) {
4883
+ if ("value" in node && typeof node.value === "string") {
4884
+ return node.value;
4885
+ }
4886
+ if (node.type === "image" || node.type === "imageReference") {
4887
+ return getAltText(node);
4888
+ }
4889
+ if (node.type === "break") {
4890
+ return " ";
4891
+ }
4892
+ if ("children" in node) {
4893
+ return node.children.map(toPlainText).join("");
4894
+ }
4895
+ return "";
4896
+ }
4897
+ function isAbsoluteUrl(url) {
4898
+ return /^(?:https?:\/\/|mailto:)/i.test(url);
4899
+ }
4900
+ function collapseSoftBreaks(text) {
4901
+ const lines = text.split("\n");
4902
+ return lines.map((line, index) => {
4903
+ const withoutLeading = index > 0 ? line.trimStart() : line;
4904
+ return index < lines.length - 1 ? withoutLeading.trimEnd() : withoutLeading;
4905
+ }).join(" ");
4906
+ }
4907
+ function tableToPlainText(table) {
4908
+ return table.children.map((row) => row.children.map((cell) => toPlainText(cell).trim()).join(" | ")).join("\n");
4909
+ }
4910
+ function escapeMrkdwn(text) {
4911
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
4912
+ }
4913
+ function escapeMrkdwnUrl(url) {
4914
+ return url.replaceAll("<", "%3C").replaceAll(">", "%3E").replaceAll("|", "%7C");
4915
+ }
4916
+ function formatMrkdwnLink(url, label) {
4917
+ if (!isAbsoluteUrl(url)) {
4918
+ return escapeMrkdwn(label);
4919
+ }
4920
+ const safeUrl = escapeMrkdwnUrl(url);
4921
+ return label && label !== url ? `<${safeUrl}|${escapeMrkdwn(label)}>` : `<${safeUrl}>`;
4922
+ }
4923
+ function wrapMrkdwn(marker, children, active) {
4924
+ const inner = renderInlineMrkdwn(children, /* @__PURE__ */ new Set([...active, marker]));
4925
+ if (active.has(marker)) {
4926
+ return inner;
4927
+ }
4928
+ const content = inner.trim();
4929
+ if (!content) {
4930
+ return inner;
4931
+ }
4932
+ const start = inner.indexOf(content);
4933
+ return `${inner.slice(0, start)}${marker}${content}${marker}${inner.slice(start + content.length)}`;
4934
+ }
4935
+ function renderInlineNodeMrkdwn(node, active) {
4936
+ switch (node.type) {
4937
+ case "text":
4938
+ return escapeMrkdwn(collapseSoftBreaks(node.value));
4939
+ case "strong":
4940
+ return wrapMrkdwn("*", node.children, active);
4941
+ case "emphasis":
4942
+ return wrapMrkdwn("_", node.children, active);
4943
+ case "delete":
4944
+ return wrapMrkdwn("~", node.children, active);
4945
+ case "inlineCode":
4946
+ return `\`${escapeMrkdwn(node.value)}\``;
4947
+ case "break":
4948
+ return "\n";
4949
+ case "link":
4950
+ return formatMrkdwnLink(node.url, toPlainText(node));
4951
+ case "image":
4952
+ return formatMrkdwnLink(node.url, getAltText(node));
4953
+ case "linkReference":
4954
+ return renderInlineMrkdwn(node.children, active);
4955
+ case "imageReference":
4956
+ return escapeMrkdwn(getAltText(node));
4957
+ case "html":
4958
+ return escapeMrkdwn(node.value);
4959
+ case "footnoteReference":
4960
+ return `[^${escapeMrkdwn(node.identifier)}]`;
4961
+ }
4962
+ }
4963
+ function renderInlineMrkdwn(nodes, active = /* @__PURE__ */ new Set()) {
4964
+ return nodes.map((node) => renderInlineNodeMrkdwn(node, active)).join("");
4965
+ }
4966
+ function quoteMrkdwn(text) {
4967
+ return text.split("\n").map((line) => `> ${line}`).join("\n");
4968
+ }
4969
+ function renderListItemMrkdwn(item, list, index, depth) {
4970
+ const indent = " ".repeat(depth);
4971
+ const bullet = depth === 0 ? "\u2022" : "\u25E6";
4972
+ const marker = list.ordered ? `${getListItemNumber(list, index)}.` : bullet;
4973
+ const { head, body } = getListItemParts(item);
4974
+ const lines = [`${indent}${marker} ${getCheckboxPrefix(item)}${renderInlineMrkdwn(head)}`.trimEnd()];
4975
+ for (const child of body) {
4976
+ if (child.type === "list") {
4977
+ child.children.forEach((subItem, subIndex) => lines.push(renderListItemMrkdwn(subItem, child, subIndex, depth + 1)));
4978
+ continue;
4979
+ }
4980
+ const rendered = renderBlockMrkdwn(child);
4981
+ if (rendered) {
4982
+ lines.push(quoteMrkdwn(rendered));
4983
+ }
4984
+ }
4985
+ return lines.join("\n");
4986
+ }
4987
+ function renderBlockMrkdwn(node) {
4988
+ switch (node.type) {
4989
+ case "heading": {
4990
+ const text = renderInlineMrkdwn(node.children, /* @__PURE__ */ new Set(["*"])).trim();
4991
+ return text ? `*${text}*` : "";
4992
+ }
4993
+ case "paragraph":
4994
+ return renderInlineMrkdwn(node.children);
4995
+ case "list":
4996
+ return node.children.map((item, index) => renderListItemMrkdwn(item, node, index, 0)).join("\n");
4997
+ case "blockquote":
4998
+ return quoteMrkdwn(node.children.map(renderBlockMrkdwn).filter(Boolean).join("\n"));
4999
+ case "code":
5000
+ return `\`\`\`
5001
+ ${escapeMrkdwn(node.value)}
5002
+ \`\`\``;
5003
+ case "html":
5004
+ return escapeMrkdwn(node.value);
5005
+ case "table":
5006
+ return escapeMrkdwn(tableToPlainText(node));
5007
+ default:
5008
+ return "";
5009
+ }
5010
+ }
5011
+ function renderUnitMrkdwn(unit) {
5012
+ if (unit.type === "item") {
5013
+ return renderListItemMrkdwn(unit.node, unit.list, unit.index, 0);
5014
+ }
5015
+ return renderBlockMrkdwn(unit.node);
5016
+ }
5017
+ function joinMrkdwnUnits(rendered) {
5018
+ let text = "";
5019
+ rendered.forEach((current, index) => {
5020
+ const previous = rendered[index - 1];
5021
+ if (previous) {
5022
+ const sameList = previous.unit.type === "item" && current.unit.type === "item" && previous.unit.list === current.unit.list;
5023
+ text += previous.unit.type === "heading" || sameList ? "\n" : "\n\n";
5024
+ }
5025
+ text += current.output;
5026
+ });
5027
+ return text;
5028
+ }
5029
+ function markdownToSlackMrkdwn(markdown, { maxLength, cutCommits = false } = {}) {
5030
+ if (!markdown.trim()) {
5031
+ return "";
5032
+ }
5033
+ const rendered = splitIntoUnits(parseMarkdown(markdown)).map((unit) => ({ unit, output: renderUnitMrkdwn(unit), length: 0 })).filter(({ output }) => output !== "");
5034
+ const full = joinMrkdwnUnits(rendered);
5035
+ if (maxLength === void 0 || full.length <= maxLength) {
5036
+ return full;
5037
+ }
5038
+ for (let count = rendered.length - 1; count >= 0; count--) {
5039
+ const next = rendered[count];
5040
+ if (cutCommits && next.unit.type !== "heading") {
5041
+ const note2 = formatOmittedChanges(countOmittedChanges(rendered.slice(count + 1).map(({ unit }) => unit)));
5042
+ const suffix = note2 ? `
5043
+
5044
+ _${note2}_` : "";
5045
+ const prefix = joinMrkdwnUnits([...rendered.slice(0, count), { ...next, output: "" }]);
5046
+ const cut = cutMrkdwn(next.output, maxLength - prefix.length - suffix.length - ELLIPSIS.length);
5047
+ if (cut) {
5048
+ return `${prefix}${cut}${ELLIPSIS}${suffix}`;
5049
+ }
5050
+ }
5051
+ const kept = dropTrailingHeadings(rendered.slice(0, count));
5052
+ if (kept.length === 0) {
5053
+ continue;
5054
+ }
5055
+ const note = formatOmittedChanges(countOmittedChanges(rendered.slice(kept.length).map(({ unit }) => unit)));
5056
+ const candidate = note ? `${joinMrkdwnUnits(kept)}
5057
+
5058
+ _${note}_` : joinMrkdwnUnits(kept);
5059
+ if (candidate.length <= maxLength) {
5060
+ return candidate;
5061
+ }
5062
+ }
5063
+ return `${sliceSlackText(full, maxLength - 3)}...`;
5064
+ }
5065
+ function cutMrkdwn(text, budget) {
5066
+ if (budget <= 0) {
5067
+ return "";
5068
+ }
5069
+ let cut = sliceSlackText(text, budget);
5070
+ const linkStart = cut.lastIndexOf("<");
5071
+ if (linkStart > cut.lastIndexOf(">")) {
5072
+ cut = cut.slice(0, linkStart);
5073
+ }
5074
+ const entityStart = cut.lastIndexOf("&");
5075
+ if (entityStart > cut.lastIndexOf(";")) {
5076
+ cut = cut.slice(0, entityStart);
5077
+ }
5078
+ cut = cut.trimEnd();
5079
+ return /[^\s•◦>]/.test(cut) ? cut : "";
5080
+ }
5081
+ function hasStyle(style) {
5082
+ return Object.values(style).some(Boolean);
5083
+ }
5084
+ function pushText(elements, text, style) {
5085
+ if (!text) {
5086
+ return;
5087
+ }
5088
+ const previous = elements.at(-1);
5089
+ if (previous?.type === "text" && JSON.stringify(previous.style ?? {}) === JSON.stringify(style)) {
5090
+ previous.text += text;
5091
+ return;
5092
+ }
5093
+ elements.push(hasStyle(style) ? { type: "text", text, style: { ...style } } : { type: "text", text });
5094
+ }
5095
+ function pushLink(elements, url, text, style) {
5096
+ if (!isAbsoluteUrl(url)) {
5097
+ pushText(elements, text, style);
5098
+ return;
5099
+ }
5100
+ const link = { type: "link", url };
5101
+ if (text && text !== url) {
5102
+ link.text = text;
5103
+ }
5104
+ if (hasStyle(style)) {
5105
+ link.style = { ...style };
5106
+ }
5107
+ elements.push(link);
5108
+ }
5109
+ function appendInlineRichText(elements, node, style) {
5110
+ switch (node.type) {
5111
+ case "text":
5112
+ pushText(elements, collapseSoftBreaks(node.value), style);
5113
+ break;
5114
+ case "strong":
5115
+ node.children.forEach((child) => appendInlineRichText(elements, child, { ...style, bold: true }));
5116
+ break;
5117
+ case "emphasis":
5118
+ node.children.forEach((child) => appendInlineRichText(elements, child, { ...style, italic: true }));
5119
+ break;
5120
+ case "delete":
5121
+ node.children.forEach((child) => appendInlineRichText(elements, child, { ...style, strike: true }));
5122
+ break;
5123
+ case "inlineCode":
5124
+ pushText(elements, node.value, { ...style, code: true });
5125
+ break;
5126
+ case "break":
5127
+ pushText(elements, "\n", style);
5128
+ break;
5129
+ case "link":
5130
+ pushLink(elements, node.url, toPlainText(node), style);
5131
+ break;
5132
+ case "image":
5133
+ pushLink(elements, node.url, getAltText(node), style);
5134
+ break;
5135
+ case "linkReference":
5136
+ node.children.forEach((child) => appendInlineRichText(elements, child, style));
5137
+ break;
5138
+ case "imageReference":
5139
+ pushText(elements, getAltText(node), style);
5140
+ break;
5141
+ case "html":
5142
+ pushText(elements, node.value, style);
5143
+ break;
5144
+ case "footnoteReference":
5145
+ pushText(elements, `[^${node.identifier}]`, style);
5146
+ break;
5147
+ }
5148
+ }
5149
+ function renderInlineRichText(nodes, style = {}, prefix = "") {
5150
+ const elements = [];
5151
+ pushText(elements, prefix, style);
5152
+ nodes.forEach((node) => appendInlineRichText(elements, node, style));
5153
+ return elements;
5154
+ }
5155
+ function ensureNotEmpty(elements) {
5156
+ return elements.length > 0 ? elements : [{ type: "text", text: " " }];
5157
+ }
5158
+ function preformatted(value) {
5159
+ return value ? [{ type: "rich_text_preformatted", elements: [{ type: "text", text: value }] }] : [];
5160
+ }
5161
+ function quote(elements) {
5162
+ return elements.length > 0 ? [{ type: "rich_text_quote", elements }] : [];
5163
+ }
5164
+ function blockquoteInline(nodes) {
5165
+ const elements = [];
5166
+ nodes.forEach((node, index) => {
5167
+ if (index > 0) {
5168
+ pushText(elements, "\n", {});
5169
+ }
5170
+ if (node.type === "paragraph" || node.type === "heading") {
5171
+ node.children.forEach((child) => appendInlineRichText(elements, child, node.type === "heading" ? { bold: true } : {}));
5172
+ } else {
5173
+ pushText(elements, toPlainText(node), {});
5174
+ }
5175
+ });
5176
+ return elements;
5177
+ }
5178
+ function renderBodyBlockRichText(node) {
5179
+ switch (node.type) {
5180
+ case "paragraph":
5181
+ return quote(renderInlineRichText(node.children));
5182
+ case "heading":
5183
+ return [{ type: "rich_text_section", elements: ensureNotEmpty(renderInlineRichText(node.children, { bold: true })) }];
5184
+ case "blockquote":
5185
+ return quote(blockquoteInline(node.children));
5186
+ case "code":
5187
+ return preformatted(node.value);
5188
+ case "html":
5189
+ return quote(renderInlineRichText([], {}, node.value));
5190
+ case "table":
5191
+ return preformatted(tableToPlainText(node));
5192
+ default:
5193
+ return [];
5194
+ }
5195
+ }
5196
+ function renderListItemRichText(item, list, index, indent) {
5197
+ const { head, body } = getListItemParts(item);
5198
+ const listElement = {
5199
+ type: "rich_text_list",
5200
+ style: list.ordered ? "ordered" : "bullet",
5201
+ elements: [{ type: "rich_text_section", elements: ensureNotEmpty(renderInlineRichText(head, {}, getCheckboxPrefix(item))) }]
5202
+ };
5203
+ if (indent > 0) {
5204
+ listElement.indent = Math.min(indent, SLACK_MAX_LIST_INDENT);
5205
+ }
5206
+ const offset = list.ordered ? getListItemNumber(list, index) - 1 : 0;
5207
+ if (offset > 0) {
5208
+ listElement.offset = offset;
5209
+ }
5210
+ const elements = [listElement];
5211
+ for (const child of body) {
5212
+ if (child.type === "list") {
5213
+ child.children.forEach((subItem, subIndex) => elements.push(...renderListItemRichText(subItem, child, subIndex, indent + 1)));
5214
+ } else {
5215
+ elements.push(...renderBodyBlockRichText(child));
5216
+ }
5217
+ }
5218
+ return elements;
5219
+ }
5220
+ function renderUnitRichText(unit) {
5221
+ if (unit.type === "heading") {
5222
+ const elements = renderInlineRichText(unit.node.children, { bold: true });
5223
+ return elements.length > 0 ? [{ type: "rich_text_section", elements }] : [];
5224
+ }
5225
+ if (unit.type === "item") {
5226
+ return renderListItemRichText(unit.node, unit.list, unit.index, 0);
5227
+ }
5228
+ const node = unit.node;
5229
+ switch (node.type) {
5230
+ case "paragraph":
5231
+ return [{ type: "rich_text_section", elements: ensureNotEmpty(renderInlineRichText(node.children)) }];
5232
+ case "blockquote":
5233
+ return quote(blockquoteInline(node.children));
5234
+ case "code":
5235
+ return preformatted(node.value);
5236
+ case "html":
5237
+ return [{ type: "rich_text_section", elements: ensureNotEmpty(renderInlineRichText([], {}, node.value)) }];
5238
+ case "table":
5239
+ return preformatted(tableToPlainText(node));
5240
+ default:
5241
+ return [];
5242
+ }
5243
+ }
5244
+ function inlineLength(elements) {
5245
+ return elements.reduce((total, element) => total + (element.type === "text" ? element.text.length : (element.text ?? element.url).length), 0);
5246
+ }
5247
+ function elementLength(element) {
5248
+ return richTextToPlainText({ elements: [element] }).length + 1;
5249
+ }
5250
+ function blocksLength(blocks) {
5251
+ return blocks.map(richTextToPlainText).join("\n\n").length;
5252
+ }
5253
+ function isSameListLevel(previous, element) {
5254
+ return previous?.type === "rich_text_list" && element.type === "rich_text_list" && previous.style === element.style && (previous.indent ?? 0) === (element.indent ?? 0);
5255
+ }
5256
+ function isSameInlineContainer(previous, element) {
5257
+ return (element.type === "rich_text_section" || element.type === "rich_text_quote") && previous?.type === element.type;
5258
+ }
5259
+ function appendElement(elements, element) {
5260
+ const previous = elements.at(-1);
5261
+ if (isSameListLevel(previous, element) && element.type === "rich_text_list") {
5262
+ previous.elements.push(...structuredClone(element.elements));
5263
+ return;
5264
+ }
5265
+ if (isSameInlineContainer(previous, element) && element.type !== "rich_text_list") {
5266
+ const last = previous.elements.at(-1);
5267
+ const separatorStyle = last?.type === "text" ? last.style ?? {} : {};
5268
+ pushText(previous.elements, element.type === "rich_text_quote" ? "\n\n" : "\n", separatorStyle);
5269
+ for (const inline of structuredClone(element.elements)) {
5270
+ if (inline.type === "text") {
5271
+ pushText(previous.elements, inline.text, inline.style ?? {});
5272
+ } else {
5273
+ previous.elements.push(inline);
5274
+ }
5275
+ }
5276
+ return;
5277
+ }
5278
+ elements.push(structuredClone(element));
5279
+ }
5280
+ function assembleRichTextBlocks(rendered, blockMaxLength) {
5281
+ const blocks = [];
5282
+ let current;
5283
+ let currentLength = 0;
5284
+ let currentHasContent = false;
5285
+ for (const { unit, output, length } of rendered) {
5286
+ const startsSection = unit.type === "heading" && currentHasContent;
5287
+ const overflows = currentHasContent && currentLength + length > blockMaxLength;
5288
+ if (!current || startsSection || overflows) {
5289
+ current = { type: "rich_text", elements: [] };
5290
+ blocks.push(current);
5291
+ currentLength = 0;
5292
+ currentHasContent = false;
5293
+ }
5294
+ output.forEach((element) => appendElement(current.elements, element));
5295
+ currentLength += length;
5296
+ currentHasContent ||= unit.type !== "heading";
5297
+ }
5298
+ return blocks;
5299
+ }
5300
+ function takeInlineCharacters(elements, count) {
5301
+ const kept = [];
5302
+ let remaining = count;
5303
+ for (const element of elements) {
5304
+ const length = inlineLength([element]);
5305
+ if (length <= remaining) {
5306
+ kept.push(structuredClone(element));
5307
+ remaining -= length;
5308
+ continue;
5309
+ }
5310
+ if (element.type === "text") {
5311
+ pushText(kept, sliceSlackText(element.text, remaining), element.style ?? {});
5312
+ }
5313
+ break;
5314
+ }
5315
+ return kept;
5316
+ }
5317
+ function takeElementCharacters(element, count) {
5318
+ const inlines = element.type === "rich_text_list" ? element.elements[0].elements : element.elements;
5319
+ const elements = takeInlineCharacters(inlines, count);
5320
+ if (elements.length === 0) {
5321
+ return void 0;
5322
+ }
5323
+ return element.type === "rich_text_list" ? { ...element, elements: [{ type: "rich_text_section", elements }] } : { ...element, elements };
5324
+ }
5325
+ function withEllipsis(element) {
5326
+ const result = structuredClone(element);
5327
+ const inlines = result.type === "rich_text_list" ? result.elements.at(-1).elements : result.elements;
5328
+ const last = inlines.at(-1);
5329
+ if (last?.type === "text") {
5330
+ last.text = last.text.trimEnd();
5331
+ if (!last.text) {
5332
+ inlines.pop();
5333
+ }
5334
+ }
5335
+ pushText(inlines, ELLIPSIS, {});
5336
+ return result;
5337
+ }
5338
+ function cutRichTextElement(element, budget) {
5339
+ const characters = element.type === "rich_text_list" ? element.elements.reduce((total, section) => total + inlineLength(section.elements), 0) : inlineLength(element.elements);
5340
+ let best;
5341
+ let low = 1;
5342
+ let high = characters - 1;
5343
+ while (low <= high) {
5344
+ const count = Math.floor((low + high) / 2);
5345
+ const taken = takeElementCharacters(element, count);
5346
+ const candidate = taken && withEllipsis(taken);
5347
+ if (candidate && elementLength(candidate) <= budget) {
5348
+ best = candidate;
5349
+ low = count + 1;
5350
+ } else {
5351
+ high = count - 1;
5352
+ }
5353
+ }
5354
+ return best;
5355
+ }
5356
+ function cutRichTextElements(elements, budget) {
5357
+ const kept = [];
5358
+ let remaining = budget;
5359
+ for (const element of elements) {
5360
+ const length = elementLength(element);
5361
+ if (length + ELLIPSIS.length <= remaining) {
5362
+ kept.push(structuredClone(element));
5363
+ remaining -= length;
5364
+ continue;
5365
+ }
5366
+ const cut = cutRichTextElement(element, remaining);
5367
+ if (cut) {
5368
+ return [...kept, cut];
5369
+ }
5370
+ break;
5371
+ }
5372
+ const last = kept.pop();
5373
+ return last ? [...kept, withEllipsis(last)] : [];
5374
+ }
5375
+ function markdownToSlackRichText(markdown, {
5376
+ maxLength,
5377
+ maxBlocks = SLACK_MAX_BLOCKS,
5378
+ maxPayloadLength,
5379
+ blockMaxLength = SLACK_RICH_TEXT_BLOCK_MAX_LENGTH
5380
+ } = {}) {
5381
+ if (!markdown.trim() || maxBlocks < 1) {
5382
+ return [];
5383
+ }
5384
+ const rendered = splitIntoUnits(parseMarkdown(markdown)).map((unit) => {
5385
+ const output = renderUnitRichText(unit);
5386
+ return { unit, output, length: output.reduce((total, element) => total + elementLength(element), 0) };
5387
+ }).filter(({ output }) => output.length > 0);
5388
+ const totalLength = (units) => units.reduce((total, { length }) => total + length, 0);
5389
+ const fits = (candidate) => candidate.length <= maxBlocks && (maxLength === void 0 || blocksLength(candidate) <= maxLength) && (maxPayloadLength === void 0 || JSON.stringify(candidate).length <= maxPayloadLength);
5390
+ const blocks = assembleRichTextBlocks(rendered, blockMaxLength);
5391
+ if (fits(blocks)) {
5392
+ return blocks;
5393
+ }
5394
+ const truncate = (chars) => {
5395
+ let remaining = chars;
5396
+ let index = 0;
5397
+ const kept = [];
5398
+ for (; index < rendered.length && rendered[index].length <= remaining; index++) {
5399
+ kept.push(rendered[index]);
5400
+ remaining -= rendered[index].length;
5401
+ }
5402
+ const next = rendered[index];
5403
+ const cut = next && next.unit.type !== "heading" ? cutRichTextElements(next.output, remaining) : [];
5404
+ const visible = cut.length > 0 ? [...kept, { unit: next.unit, output: cut, length: cut.reduce((total, element) => total + elementLength(element), 0) }] : dropTrailingHeadings(kept);
5405
+ const note = formatOmittedChanges(countOmittedChanges(rendered.slice(cut.length > 0 ? index + 1 : visible.length).map(({ unit }) => unit)));
5406
+ const truncated = assembleRichTextBlocks(visible, blockMaxLength);
5407
+ if (note) {
5408
+ const noteSection = { type: "rich_text_section", elements: [{ type: "text", text: note, style: { italic: true } }] };
5409
+ const last = truncated.at(-1);
5410
+ if (last) {
5411
+ last.elements.push(noteSection);
5412
+ } else {
5413
+ truncated.push({ type: "rich_text", elements: [noteSection] });
5414
+ }
5415
+ }
5416
+ return truncated;
5417
+ };
5418
+ let best = truncate(0);
5419
+ let low = 1;
5420
+ let high = totalLength(rendered) - 1;
5421
+ while (low <= high) {
5422
+ const chars = Math.floor((low + high) / 2);
5423
+ const candidate = truncate(chars);
5424
+ if (fits(candidate)) {
5425
+ best = candidate;
5426
+ low = chars + 1;
5427
+ } else {
5428
+ high = chars - 1;
5429
+ }
5430
+ }
5431
+ return best;
5432
+ }
5433
+ function inlineToPlainText(elements) {
5434
+ return elements.map((element) => element.type === "text" ? element.text : element.text ?? element.url).join("");
5435
+ }
5436
+ function richTextToPlainText(block) {
5437
+ return block.elements.map((element) => {
5438
+ switch (element.type) {
5439
+ case "rich_text_list":
5440
+ return element.elements.map((section, index) => {
5441
+ const indent = " ".repeat(element.indent ?? 0);
5442
+ const marker = element.style === "ordered" ? `${(element.offset ?? 0) + index + 1}.` : "\u2022";
5443
+ return `${indent}${marker} ${inlineToPlainText(section.elements)}`;
5444
+ }).join("\n");
5445
+ case "rich_text_quote":
5446
+ return quoteMrkdwn(inlineToPlainText(element.elements));
5447
+ case "rich_text_preformatted":
5448
+ return `\`\`\`
5449
+ ${inlineToPlainText(element.elements)}
5450
+ \`\`\``;
5451
+ default:
5452
+ return inlineToPlainText(element.elements);
5453
+ }
5454
+ }).join("\n");
5455
+ }
5456
+
4708
5457
  function extractChangelogSummary(changelog, { stripBoldMarkers = false, maxLength } = {}) {
4709
5458
  if (changelog.trim() === "") {
4710
5459
  return "";
@@ -4755,14 +5504,12 @@ function getSlackToken(options) {
4755
5504
  function getSlackWebhookUrl(options) {
4756
5505
  return options.socialWebhookUrl || process.env.RELIZY_SLACK_WEBHOOK_URL || process.env.SLACK_WEBHOOK_URL || null;
4757
5506
  }
4758
- function formatChangelogForSlack(changelog, maxLength = 2500) {
4759
- let formatted = changelog.replace(/^### (.+)$/gm, "*$1*").replace(/^## (.+)$/gm, "*$1*").replace(/^# (.+)$/gm, "*$1*").replace(/\*\*(.+?)\*\*/g, "*$1*").replace(/^([ \t]*)-[ \t]+/gm, "$1\u2022 ");
4760
- const linkPattern = /\[([^\]]*)]\(([^)]*)\)/g;
4761
- formatted = formatted.replace(linkPattern, (_, text, url) => `<${url}|${text}>`);
4762
- if (formatted.length > maxLength) {
4763
- formatted = `${formatted.substring(0, maxLength - 3)}...`;
4764
- }
4765
- return formatted;
5507
+ const SLACK_SUMMARY_MAX_LENGTH = 2500;
5508
+ function resolveSlackPostMaxLength({ changelogFormat = "summary", postMaxLength }) {
5509
+ return postMaxLength ?? (changelogFormat === "full" ? void 0 : SLACK_SUMMARY_MAX_LENGTH);
5510
+ }
5511
+ function formatChangelogForSlack(changelog, maxLength = SLACK_SUMMARY_MAX_LENGTH) {
5512
+ return markdownToSlackMrkdwn(changelog, { maxLength });
4766
5513
  }
4767
5514
  function formatPackagesForSlack(packages) {
4768
5515
  if (packages.length === 0) {
@@ -4772,75 +5519,96 @@ function formatPackagesForSlack(packages) {
4772
5519
  (pkg) => pkg.hasTransition ? `\u2022 *${pkg.name}* : \`${pkg.oldVersion}\` \u2192 \`${pkg.newVersion}\`` : `\u2022 *${pkg.name}* : \`${pkg.version}\``
4773
5520
  ).join("\n");
4774
5521
  }
4775
- function formatSlackMessage({ projectName, version, changelog, releaseUrl, changelogUrl, template, contributors = [], packages = [], postMaxLength = 2500 }) {
5522
+ function truncateSlackText(text, maxLength) {
5523
+ return text.length > maxLength ? `${sliceSlackText(text, maxLength - 1)}\u2026` : text;
5524
+ }
5525
+ function formatListSection(title, lines, noun) {
5526
+ const render = (count) => {
5527
+ const omitted = lines.length - count;
5528
+ const kept = lines.slice(0, count);
5529
+ if (omitted > 0) {
5530
+ kept.push(`_\u2026 and ${omitted} more ${noun}_`);
5531
+ }
5532
+ return `${title}
5533
+
5534
+ ${kept.join("\n")}`;
5535
+ };
5536
+ for (let count = lines.length; count > 0; count--) {
5537
+ const text = render(count);
5538
+ if (text.length <= SLACK_SECTION_MAX_LENGTH) {
5539
+ return text;
5540
+ }
5541
+ }
5542
+ return truncateSlackText(render(0), SLACK_SECTION_MAX_LENGTH);
5543
+ }
5544
+ function renderSlackTemplate({ template, projectName, version, changelog, contributorsLine, packagesLine, releaseUrl, changelogUrl }) {
5545
+ let message = template.replace("{{projectName}}", projectName).replace("{{newVersion}}", version).replace("{{changelog}}", changelog).replace("{{contributors}}", contributorsLine).replace("{{packages}}", packagesLine);
5546
+ if (releaseUrl) {
5547
+ message = message.replace("{{releaseUrl}}", releaseUrl);
5548
+ } else {
5549
+ message = message.replace("{{releaseUrl}}", "").trim();
5550
+ }
5551
+ if (changelogUrl) {
5552
+ message = message.replace("{{changelogUrl}}", changelogUrl);
5553
+ } else {
5554
+ message = message.replace("{{changelogUrl}}", "").trim();
5555
+ }
5556
+ return message;
5557
+ }
5558
+ function formatSlackMessage({ projectName, version, changelog, releaseUrl, changelogUrl, template, contributors = [], packages = [], postMaxLength, changelogFormat = "summary" }) {
4776
5559
  const contributorsLine = contributors.length > 0 ? contributors.map((n) => `\u2022 ${n}`).join("\n") : "";
4777
5560
  const packagesLine = formatPackagesForSlack(packages);
5561
+ const maxLength = resolveSlackPostMaxLength({ changelogFormat, postMaxLength });
4778
5562
  if (template) {
4779
- const summary = extractChangelogSummary(changelog, { maxLength: postMaxLength });
4780
- let message = template.replace("{{projectName}}", projectName).replace("{{newVersion}}", version).replace("{{changelog}}", summary).replace("{{contributors}}", contributorsLine).replace("{{packages}}", packagesLine);
4781
- if (releaseUrl) {
4782
- message = message.replace("{{releaseUrl}}", releaseUrl);
4783
- } else {
4784
- message = message.replace("{{releaseUrl}}", "").trim();
4785
- }
4786
- if (changelogUrl) {
4787
- message = message.replace("{{changelogUrl}}", changelogUrl);
5563
+ const templateContext = { template, projectName, version, contributorsLine, packagesLine, releaseUrl, changelogUrl };
5564
+ let changelogText;
5565
+ if (changelogFormat === "full") {
5566
+ const overhead = renderSlackTemplate({ ...templateContext, changelog: "\0" }).length - 1;
5567
+ changelogText = markdownToSlackMrkdwn(changelog, {
5568
+ maxLength: Math.max(0, Math.min(maxLength ?? Infinity, SLACK_SECTION_MAX_LENGTH - overhead)),
5569
+ cutCommits: true
5570
+ });
4788
5571
  } else {
4789
- message = message.replace("{{changelogUrl}}", "").trim();
5572
+ changelogText = extractChangelogSummary(changelog, { maxLength });
4790
5573
  }
4791
5574
  return [
4792
5575
  {
4793
5576
  type: "section",
4794
5577
  text: {
4795
5578
  type: "mrkdwn",
4796
- text: message
5579
+ text: truncateSlackText(renderSlackTemplate({ ...templateContext, changelog: changelogText }), SLACK_SECTION_MAX_LENGTH)
4797
5580
  }
4798
5581
  }
4799
5582
  ];
4800
5583
  }
4801
- const blocks = [
4802
- {
4803
- type: "header",
4804
- text: {
4805
- type: "plain_text",
4806
- text: `\u{1F4E3} ${projectName} ${version} is out!`,
4807
- emoji: true
4808
- }
5584
+ const headerBlock = {
5585
+ type: "header",
5586
+ text: {
5587
+ type: "plain_text",
5588
+ text: truncateSlackText(`\u{1F4E3} ${projectName} ${version} is out!`, SLACK_HEADER_MAX_LENGTH),
5589
+ emoji: true
4809
5590
  }
4810
- ];
4811
- const formattedChangelog = formatChangelogForSlack(changelog, postMaxLength);
4812
- if (formattedChangelog) {
4813
- blocks.push({
4814
- type: "section",
4815
- text: {
4816
- type: "mrkdwn",
4817
- text: formattedChangelog
4818
- }
4819
- });
4820
- }
5591
+ };
5592
+ const trailingBlocks = [];
4821
5593
  if (packages.length > 0) {
4822
- blocks.push({
5594
+ trailingBlocks.push({
4823
5595
  type: "section",
4824
5596
  text: {
4825
5597
  type: "mrkdwn",
4826
- text: `*\u{1F4E6} Packages*
4827
-
4828
- ${packagesLine}`
5598
+ text: formatListSection("*\u{1F4E6} Packages*", packagesLine.split("\n"), "packages")
4829
5599
  }
4830
5600
  });
4831
5601
  }
4832
5602
  if (contributors.length > 0) {
4833
- blocks.push({
5603
+ trailingBlocks.push({
4834
5604
  type: "section",
4835
5605
  text: {
4836
5606
  type: "mrkdwn",
4837
- text: `*\u2764\uFE0F Contributors*
4838
-
4839
- ${contributorsLine}`
5607
+ text: formatListSection("*\u2764\uFE0F Contributors*", contributorsLine.split("\n"), "contributors")
4840
5608
  }
4841
5609
  });
4842
5610
  }
4843
- blocks.push({
5611
+ trailingBlocks.push({
4844
5612
  type: "divider"
4845
5613
  });
4846
5614
  const elements = [];
@@ -4869,19 +5637,39 @@ ${contributorsLine}`
4869
5637
  });
4870
5638
  }
4871
5639
  if (elements.length > 0) {
4872
- blocks.push({
5640
+ trailingBlocks.push({
4873
5641
  type: "actions",
4874
5642
  elements
4875
5643
  });
4876
5644
  }
4877
- return blocks;
5645
+ const changelogBlocks = [];
5646
+ if (changelogFormat === "full") {
5647
+ const surroundingBlocks = [headerBlock, ...trailingBlocks];
5648
+ changelogBlocks.push(...markdownToSlackRichText(changelog, {
5649
+ maxLength,
5650
+ maxBlocks: SLACK_MAX_BLOCKS - surroundingBlocks.length,
5651
+ maxPayloadLength: SLACK_RICH_TEXT_MESSAGE_MAX_LENGTH - JSON.stringify(surroundingBlocks).length
5652
+ }));
5653
+ } else {
5654
+ const formattedChangelog = formatChangelogForSlack(changelog, Math.min(postMaxLength ?? SLACK_SUMMARY_MAX_LENGTH, SLACK_SECTION_MAX_LENGTH));
5655
+ if (formattedChangelog) {
5656
+ changelogBlocks.push({
5657
+ type: "section",
5658
+ text: {
5659
+ type: "mrkdwn",
5660
+ text: formattedChangelog
5661
+ }
5662
+ });
5663
+ }
5664
+ }
5665
+ return [headerBlock, ...changelogBlocks, ...trailingBlocks];
4878
5666
  }
4879
5667
  function mapWebhookError(status, body) {
4880
5668
  if (status === 404 || body.includes("no_service")) {
4881
5669
  return "The webhook URL is invalid or has been deactivated. Regenerate it in your Slack app settings.";
4882
5670
  }
4883
5671
  if (body.includes("invalid_payload")) {
4884
- return "The message payload was rejected by Slack (likely exceeds 3000 chars per block). Lower social.slack.postMaxLength.";
5672
+ return 'The message payload was rejected by Slack (likely exceeds its block limits). Lower social.slack.postMaxLength or switch social.slack.changelogFormat to "summary".';
4885
5673
  }
4886
5674
  if (body.includes("channel_not_found")) {
4887
5675
  return "The channel bound to this webhook was archived or removed. Create a new webhook.";
@@ -4953,6 +5741,7 @@ async function postReleaseToSlack({
4953
5741
  contributors,
4954
5742
  packages,
4955
5743
  postMaxLength,
5744
+ changelogFormat,
4956
5745
  dryRun = false
4957
5746
  }) {
4958
5747
  const useWebhook = Boolean(webhookUrl);
@@ -4973,12 +5762,13 @@ async function postReleaseToSlack({
4973
5762
  changelogUrl,
4974
5763
  contributors,
4975
5764
  packages,
4976
- postMaxLength
5765
+ postMaxLength,
5766
+ changelogFormat
4977
5767
  });
4978
5768
  const fallbackText = `${projectName} ${version} is out!`;
4979
5769
  logger.debug(`Message blocks (${blocks.length} blocks)`);
4980
5770
  if (dryRun) {
4981
- const preview = blocks.filter((b) => b.type === "header" || b.type === "section").map((b) => b.text?.text ?? "").filter(Boolean).join("\n\n");
5771
+ const preview = blocks.map((b) => b.type === "rich_text" ? richTextToPlainText(b) : b.text?.text ?? "").filter(Boolean).join("\n\n");
4982
5772
  const target = useWebhook ? "webhook" : `channel: ${channel}`;
4983
5773
  logger.box(`[dry-run] Slack Post Preview (${target})
4984
5774
 
@@ -6565,7 +7355,8 @@ async function handleSlackPost({
6565
7355
  template,
6566
7356
  contributors,
6567
7357
  packages,
6568
- postMaxLength: slackConfig.postMaxLength ?? 2500,
7358
+ postMaxLength: slackConfig.postMaxLength,
7359
+ changelogFormat: slackConfig.changelogFormat ?? "summary",
6569
7360
  dryRun
6570
7361
  });
6571
7362
  await executeHook("success:slack", config, dryRun);
@@ -6596,6 +7387,9 @@ async function social(options = {}) {
6596
7387
  }
6597
7388
  });
6598
7389
  applyAIOverride(config, options.ai);
7390
+ if (options.slackFormat) {
7391
+ config.social.slack = { ...config.social.slack, changelogFormat: options.slackFormat };
7392
+ }
6599
7393
  logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
6600
7394
  await socialSafetyCheck({ config });
6601
7395
  if (!config.release.social && !config.social?.twitter?.enabled && !config.social?.slack?.enabled) {
@@ -6754,13 +7548,16 @@ async function social(options = {}) {
6754
7548
  ${twitterChangelog}`);
6755
7549
  }
6756
7550
  }
6757
- let slackChangelog = minifiedBody;
7551
+ const slackChangelogFormat = config.social?.slack?.changelogFormat ?? "summary";
7552
+ const slackBody = slackChangelogFormat === "full" ? richBody : minifiedBody;
7553
+ let slackChangelog = slackBody;
6758
7554
  if (hasContent && slackWillPost && isAISocialEnabled(config, "slack")) {
6759
7555
  slackChangelog = await generateAISocialChangelog({
6760
7556
  config,
6761
7557
  rawBody: richBody,
6762
- fallbackBody: minifiedBody,
6763
- platform: "slack"
7558
+ fallbackBody: slackBody,
7559
+ platform: "slack",
7560
+ maxLength: resolveSlackPostMaxLength({ changelogFormat: slackChangelogFormat, postMaxLength: config.social.slack.postMaxLength })
6764
7561
  });
6765
7562
  if (dryRun) {
6766
7563
  logger.box("[dry-run] AI Slack preview", slackChangelog);
@@ -7138,8 +7935,9 @@ async function release(options = {}) {
7138
7935
  bumpResult,
7139
7936
  dryRun,
7140
7937
  logLevel: config.logLevel,
7141
- safetyCheck: false
7938
+ safetyCheck: false,
7142
7939
  // Already checked in releaseSafetyCheck
7940
+ slackFormat: options.slackFormat
7143
7941
  });
7144
7942
  } else {
7145
7943
  logger.info("Skipping social media posts (--no-social or no social media enabled)");
@@ -7184,4 +7982,4 @@ async function release(options = {}) {
7184
7982
  }
7185
7983
  }
7186
7984
 
7187
- export { getDefaultConfig as $, expandPackagesToBumpWithDependents as A, extractChangelogSummary as B, extractVersionFromPackageTag as C, extractVersionFromTag as D, fetchGitTags as E, filterOutPrivatePackages as F, filterPrivatePackagesUnlessIncluded as G, findGitHubPR as H, findGitLabMR as I, findReachableCommitBySubject as J, formatChangelogForSlack as K, formatPackagesForSlack as L, formatSlackMessage as M, NEW_PACKAGE_MARKER as N, formatTweetMessage as O, PR_COMMENT_MARKER as P, generateChangelog as Q, generateMarkDown as R, getAuthCommand as S, getBumpedIndependentPackages as T, getBumpedPackageIndependently as U, getCIName as V, getCanaryVersion as W, getCommitChangedFiles as X, getCommitSubject as Y, getCurrentGitBranch as Z, getCurrentGitRef as _, assertBranchUpToDateWithRemote as a, redactSecrets as a$, getDependentsOf as a0, getFirstCommit as a1, getGitDiff as a2, getGitStatus as a3, getGitUpstream as a4, getIndependentTag as a5, getLastPackageTag as a6, getLastRepoTag as a7, getLastStableTag as a8, getLastTag as a9, isGraduating as aA, isGraduatingToStableBetweenVersion as aB, isInCI as aC, isPrerelease as aD, isPrereleaseReleaseType as aE, isStableReleaseType as aF, isTagVersionCompatibleWithCurrent as aG, isVersionAlreadyPublishedError as aH, isVersionPublished as aI, loadRelizyConfig as aJ, mergeTypes as aK, parseChangelogMarkdown as aL, parseGitRemoteUrl as aM, postPrComment as aN, postReleaseToSlack as aO, postReleaseToTwitter as aP, prComment as aQ, providerRelease as aR, providerReleaseSafetyCheck as aS, publish as aT, publishPackage as aU, publishSafetyCheck as aV, pushCommitAndTags as aW, pushTagForce as aX, readPackageJson as aY, readPackages as aZ, reconcileFromTag as a_, getModifiedReleaseFilePatterns as aa, getNewPackageCompareBase as ab, getNpmRegistry as ac, getPackageCommits as ad, getPackageDependencies as ae, getPackageNewVersion as af, getPackages as ag, getPackagesOrBumpedPackages as ah, getPackagesToPublishInIndependentMode as ai, getPackagesToPublishInSelectiveMode as aj, getPreid as ak, getReleaseTagNames as al, getReleaseUrl as am, getRootPackage as an, getShortCommitSha as ao, getSlackToken as ap, getSlackWebhookUrl as aq, getTwitterCredentials as ar, github as as, gitlab as at, globDirsAbsolute as au, hasLernaJson as av, isAncestor as aw, isBumpedPackage as ax, isChangedPreid as ay, isCommitOnlyInIgnoredPackages as az, assertReleaseTagsAvailable as b, release as b0, resetRewrittenTagCache as b1, resolveAllConfiguredRegistryTargets as b2, resolveIgnoredPackagePaths as b3, resolveRegistryTargetsForPackage as b4, resolveRegistryTargetsForPackages as b5, resolveTags as b6, retagAnnotatedLocal as b7, rollbackModifiedFiles as b8, shouldFilterPrereleaseTags as b9, social as ba, socialSafetyCheck as bb, tagExists as bc, topologicalSort as bd, tryPostPrComment as be, updateLernaVersion as bf, withRegistryNpmrc as bg, writeChangelogToFile as bh, writeVersion as bi, buildChangelogBody as c, buildCommentBody as d, buildCompareLink as e, buildContributors as f, buildReleaseSummary as g, bump as h, capReleaseTypeForZeroMajor as i, changelog as j, checkGitStatusIfDirty as k, collectContributorNames as l, collectPackageBumps as m, confirmBump as n, createCommitAndTags as o, createGitlabRelease as p, defineConfig as q, detectGitProvider as r, detectPackageManager as s, detectPullRequest as t, determinePublishTag as u, determineReleaseType as v, determineSemverChange as w, executeBuildCmd as x, executeFormatCmd as y, executeHook as z };
7985
+ export { getBumpedPackageIndependently as $, determineReleaseType as A, determineSemverChange as B, executeBuildCmd as C, executeFormatCmd as D, executeHook as E, expandPackagesToBumpWithDependents as F, extractChangelogSummary as G, extractVersionFromPackageTag$1 as H, extractVersionFromRepoTag as I, extractVersionFromTag as J, fetchGitTags as K, filterOutPrivatePackages as L, filterPrivatePackagesUnlessIncluded as M, NEW_PACKAGE_MARKER as N, findGitHubPR as O, PR_COMMENT_MARKER as P, findGitLabMR as Q, findReachableCommitBySubject as R, SLACK_HEADER_MAX_LENGTH as S, formatChangelogForSlack as T, formatPackagesForSlack as U, formatSlackMessage as V, formatTweetMessage as W, generateChangelog as X, generateMarkDown as Y, getAuthCommand as Z, getBumpedIndependentPackages as _, SLACK_MAX_BLOCKS as a, providerRelease as a$, getCIName as a0, getCanaryVersion as a1, getCommitChangedFiles as a2, getCommitSubject as a3, getCurrentGitBranch as a4, getCurrentGitRef as a5, getDefaultConfig as a6, getDependentsOf as a7, getFirstCommit as a8, getGitDiff as a9, gitlab as aA, globDirsAbsolute as aB, hasLernaJson as aC, isAncestor as aD, isBumpedPackage as aE, isChangedPreid as aF, isCommitOnlyInIgnoredPackages as aG, isGraduating as aH, isGraduatingToStableBetweenVersion as aI, isInCI as aJ, isPrerelease as aK, isPrereleaseReleaseType as aL, isStableReleaseType as aM, isTagVersionCompatibleWithCurrent as aN, isVersionAlreadyPublishedError as aO, isVersionPublished as aP, listTags as aQ, loadRelizyConfig as aR, markdownToSlackMrkdwn as aS, markdownToSlackRichText as aT, mergeTypes as aU, parseChangelogMarkdown as aV, parseGitRemoteUrl as aW, postPrComment as aX, postReleaseToSlack as aY, postReleaseToTwitter as aZ, prComment as a_, getGitStatus as aa, getGitUpstream as ab, getIndependentTag as ac, getLastPackageTag as ad, getLastRepoTag as ae, getLastStableTag as af, getLastTag as ag, getModifiedReleaseFilePatterns as ah, getNewPackageCompareBase as ai, getNpmRegistry as aj, getPackageCommits as ak, getPackageDependencies as al, getPackageNewVersion as am, getPackages as an, getPackagesOrBumpedPackages as ao, getPackagesToPublishInIndependentMode as ap, getPackagesToPublishInSelectiveMode as aq, getPreid as ar, getReleaseTagNames as as, getReleaseUrl as at, getRootPackage as au, getShortCommitSha as av, getSlackToken as aw, getSlackWebhookUrl as ax, getTwitterCredentials as ay, github as az, SLACK_RICH_TEXT_BLOCK_MAX_LENGTH as b, providerReleaseSafetyCheck as b0, publish as b1, publishPackage as b2, publishSafetyCheck as b3, pushCommitAndTags as b4, pushTagForce as b5, readPackageJson as b6, readPackages as b7, reconcileFromTag as b8, redactSecrets as b9, release as ba, resetRewrittenTagCache as bb, resolveAllConfiguredRegistryTargets as bc, resolveIgnoredPackagePaths as bd, resolveRegistryTargetsForPackage as be, resolveRegistryTargetsForPackages as bf, resolveSlackPostMaxLength as bg, resolveTags as bh, retagAnnotatedLocal as bi, richTextToPlainText as bj, rollbackModifiedFiles as bk, shouldFilterPrereleaseTags as bl, sliceSlackText as bm, social as bn, socialSafetyCheck as bo, tagExists as bp, topologicalSort as bq, tryPostPrComment as br, updateLernaVersion as bs, withRegistryNpmrc as bt, writeChangelogToFile as bu, writeVersion as bv, SLACK_RICH_TEXT_MESSAGE_MAX_LENGTH as c, SLACK_SECTION_MAX_LENGTH as d, SLACK_SUMMARY_MAX_LENGTH as e, assertBranchUpToDateWithRemote as f, assertReleaseTagsAvailable as g, buildChangelogBody as h, buildCommentBody as i, buildCompareLink as j, buildContributors as k, buildReleaseSummary as l, bump as m, capReleaseTypeForZeroMajor as n, changelog as o, checkGitStatusIfDirty as p, collectContributorNames as q, collectPackageBumps as r, confirmBump as s, createCommitAndTags as t, createGitlabRelease as u, defineConfig as v, detectGitProvider as w, detectPackageManager as x, detectPullRequest as y, determinePublishTag as z };