(body: string, releaseUrl: string)
| 332 | // --- Release notes rendering --- |
| 333 | |
| 334 | export function truncateReleaseNotes(body: string, releaseUrl: string): string { |
| 335 | const MAX_LINES = 20; |
| 336 | const MAX_CHARS = 2000; |
| 337 | |
| 338 | const normalized = body.replace(/\r\n/g, '\n').trim(); |
| 339 | if (normalized.length === 0) { |
| 340 | return `Full release notes: ${releaseUrl}`; |
| 341 | } |
| 342 | |
| 343 | const lines = normalized.split('\n'); |
| 344 | const included: string[] = []; |
| 345 | let charCount = 0; |
| 346 | let truncated = false; |
| 347 | |
| 348 | for (const line of lines) { |
| 349 | if (included.length >= MAX_LINES) { |
| 350 | truncated = true; |
| 351 | break; |
| 352 | } |
| 353 | const nextCharCount = charCount + (included.length > 0 ? 1 : 0) + line.length; |
| 354 | if (nextCharCount > MAX_CHARS && included.length > 0) { |
| 355 | truncated = true; |
| 356 | break; |
| 357 | } |
| 358 | included.push(line); |
| 359 | charCount = nextCharCount; |
| 360 | } |
| 361 | |
| 362 | let result = included.join('\n'); |
| 363 | if (truncated) { |
| 364 | result += '\n\n... (truncated)'; |
| 365 | } |
| 366 | result += `\n\nFull release notes: ${releaseUrl}`; |
| 367 | return result; |
| 368 | } |
| 369 | |
| 370 | // --- Spawn runners --- |
| 371 |
no test coverage detected