(options: GithubChangelogOptions = {})
| 47 | * "changelog": ["github", { "internalAuthors": ["theoephraim"] }] |
| 48 | */ |
| 49 | export function createGithubFormatter(options: GithubChangelogOptions = {}): ChangelogFormatter { |
| 50 | const includeCommitLink = options.includeCommitLink ?? false; |
| 51 | const thankContributors = options.thankContributors ?? true; |
| 52 | const internalAuthorsSet = new Set( |
| 53 | [...DEFAULT_INTERNAL_AUTHORS, ...(options.internalAuthors ?? [])].map((a) => a.toLowerCase()), |
| 54 | ); |
| 55 | |
| 56 | return async (ctx: ChangelogContext) => { |
| 57 | const { release, bumpFiles, date } = ctx; |
| 58 | const repoSlug = options.repo ?? detectRepo(); |
| 59 | const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'; |
| 60 | |
| 61 | const lines: string[] = []; |
| 62 | lines.push(`## ${release.newVersion}`); |
| 63 | lines.push(`<sub>${date}</sub>`); |
| 64 | lines.push(''); |
| 65 | |
| 66 | const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id)); |
| 67 | const sorted = sortBumpFilesByType(relevantBumpFiles, release.name); |
| 68 | |
| 69 | for (const bf of sorted) { |
| 70 | if (!bf.summary || isChangelogSuppressed(bf, release.name)) continue; |
| 71 | |
| 72 | const type = getBumpTypeForPackage(bf, release.name); |
| 73 | const tag = ` *(${type})*`; |
| 74 | |
| 75 | // Extract metadata overrides from summary (pr, commit, author lines) |
| 76 | const { cleanSummary, overrides } = extractSummaryMeta(bf.summary); |
| 77 | |
| 78 | // Look up git/PR info, with overrides taking precedence |
| 79 | const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides); |
| 80 | |
| 81 | const summaryLines = trimBlankEdges(cleanSummary.split('\n')); |
| 82 | const linkify = (s: string) => linkifyIssueRefs(s, serverUrl, repoSlug); |
| 83 | |
| 84 | // Build the prefix: PR link, commit link, thanks |
| 85 | const { links, thanks } = formatPrefix( |
| 86 | gitInfo, |
| 87 | serverUrl, |
| 88 | repoSlug, |
| 89 | includeCommitLink, |
| 90 | thankContributors, |
| 91 | internalAuthorsSet, |
| 92 | ); |
| 93 | |
| 94 | // Assemble metadata: links, tag, thanks |
| 95 | const parts = [links, tag, thanks].filter(Boolean); |
| 96 | const hasMeta = parts.length > 0; |
| 97 | const meta = parts.join(' '); |
| 98 | |
| 99 | if (summaryLines.length === 0) { |
| 100 | // Metadata only (e.g. summary was just `pr:`/`author:` lines) |
| 101 | if (hasMeta) lines.push(`- ${meta}`); |
| 102 | } else if (hasMeta && summaryNeedsBlockLayout(cleanSummary)) { |
| 103 | // Long/multiline/markdown summary reads poorly inline — put it on its own |
| 104 | // line(s) below the metadata. |
| 105 | lines.push(`- ${meta}`); |
| 106 | for (const sl of summaryLines) { |
no test coverage detected
searching dependent graphs…