从第三方 CHANGELOG 到 PR 变更日志:深入 Renovate 的 changelog 解析管线(以 adapter-utils.md 测试夹具为例)
2026/9/13 11:26:50 网站建设 项目流程

从第三方 CHANGELOG 到 PR 变更日志:深入 Renovate 的 changelog 解析管线(以 adapter-utils.md 测试夹具为例)

【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate

Renovate 在升级依赖并创建 Pull Request 时,会在 PR 正文中附带"变更日志"(changelog),帮助开发者快速判断这次升级改了什么。本文以仓库中一份真实的测试夹具 adapter-utils.md 为分析样本,结合 release-notes.ts、gitlab/index.ts 与 release-notes.spec.ts 的源码实现,完整讲解 Renovate 是如何发现、拉取、切分、匹配并清洗第三方项目的 CHANGELOG 文件,最终把指定版本的变更条目拼进 PR 正文的。读完本文,你将理解 Renovate 变更日志管线的完整调用链、标题解析与版本匹配策略,以及它如何用"真实世界样本"来驱动单元测试。

一、先认识这份文档:一份真实的 GitLab 项目 CHANGELOG 样本

adapter-utils.md 位于lib/workers/repository/update/pr/changelog/__fixtures__/目录,与angular-js.mdjest.mdjs-yaml.mdyargs.md等一同作为变更日志解析的测试输入。它本身并非 Renovate 的说明文档,而是itentialopensource/adapter-utils 项目 CHANGELOG.md 的逐字副本,被用作release-notes.spec.ts中 GitLab 平台解析用例的 fixture 数据。

这份样本的独特价值在于它的"不规整性":

  • 早期版本段(# Current Version: 4.3.1及之前)是典型的Keep-a-Changelog 风格## New Features## Improvements## Bug Fixes## Deprecation## Security分类齐全,且版本以__3.10.0 [12-05-2018]__这种加粗内联形式嵌在条目里;
  • 后期版本段(## 4.33.0 [05-15-2020]起)切换为逐版本平铺的紧凑格式:每个版本一个二级标题,含日期、条目、Closes ADAPT-xxx工单引用与See merge request itentialopensource/adapter-utils!177合并请求引用。

这种"同一文件内部混用两种风格"的结构,恰好是解析器最容易出错、也最值得测试覆盖的场景。

二、样本的数据形态:Renovate 需要解析的四种结构要素

逐行阅读 adapter-utils.md,可以归纳出 Renovate 解析器必须处理的全部结构要素:

2.1 逐版本平铺段(新版格式,第 2 行起)

## 4.33.0 [05-15-2020] * add new auth, fix accept header and base path in mock Closes ADAPT-207 See merge request itentialopensource/adapter-utils!177 --- ## 4.32.3 [04-30-2020] * set username and password in token entitypath Closes ADAPT-198 See merge request itentialopensource/adapter-utils!176

这段结构包含:

  • 版本标题## <版本号> [YYYY-MM-DD],版本号与日期之间是空格而非链接语法;
  • 条目列表:以*开头的无序列表,描述本次变更内容;
  • 工单引用Closes ADAPT-207形式的单行文本;
  • 合并请求引用See merge request itentialopensource/adapter-utils!177
  • 分隔线---将相邻版本隔开。

fixture 覆盖了从4.33.0 [05-15-2020]4.24.0 [11-01-2019]的完整平铺段,每一条都保持"标题 + 条目 + 引用 + 分隔线"的统一骨架,例如4.30.14记录"add in double check for starting slashes in the path",4.17.2记录"update regex for , and ()",4.9.3记录"add filter to response after field is found, based on respFilter in schema"。

2.2 分类标题段(旧版格式)

# Current Version: 4.3.1 [03-26-2019]开始,样本切换到另一种组织方式:

# Current Version: 4.3.1 [03-26-2019] ## New Features ## Improvements ## Bug Fixes ## Deprecation ## Security

在每个分类下,版本信息以__<版本> [日期]__内联加粗形式出现在条目开头,例如:

* __3.10.0 [12-05-2018]__ - New methods have been add to: ... * __3.9.0 [12-04-2018]__ - The external name on schemas can now be at the same level or lower * __2.1.0 [08-17-2018]__ - These libraries now support token re-use and expiration ...

这一段的"版本号不在标题里,而在正文条目里"的特征,正是 Renovate 需要在正文中搜索版本号的原因(见 4.3 节)。

2.3 大版本发布列表(如 2.0.0)

样本在## New Features下还包含一次大版本集中发布说明(2.0.0 [08-13-2018]),一口气列出 PH-16044、PH-16024、PH-16125、PH-15075、PH-14311、PH-16053、PH-16141、PH-16239、PH-16268、PH-15718 共 10 项改动,涵盖通用调用、mock 数据、代理能力、Base64 认证、加解密、action 数组化、entitypath 语法变更等。

2.4 外层壳:<a name>锚点与文末#\n##哨兵

源码 gitlab/index.ts 在取回 changelog 原文后,会执行:

const changelogMd = `${fileRes.body}\n#\n##`;

在文末追加\n#\n##两个伪标题作为哨兵,保证最后一节之后一定存在更低层级的标题,使切分逻辑(见 4.1 节)能正确闭合最后一段。同时 release-notes.ts 会先用正则剔除 Keep-a-Changelog 常见的<a name="..."></a>锚点行,避免其干扰标题切分。

三、前置阶段:Renovate 如何发现并拉取 CHANGELOG 文件

在解析任何内容之前,Renovate 必须先回答两个问题:changelog 文件在哪里?以及要不要拉取它?

3.1 跳过名单与缓存

release-notes.ts 定义了repositoriesToSkipMdFetching,当前包含facebook/react-nativereact/react-native——这两个仓库被显式跳过 MD 抓取,回退到 GitHub release 接口(shouldSkipChangelogMd,见 release-notes.ts)。

文件查找结果还带有两层缓存:getReleaseNotesMdFile使用内存缓存(memCache),缓存键为getReleaseNotesMdFile@v2-<repository>-<sourceDirectory>-<apiBaseUrl>(release-notes.ts);最终解析结果则进入changelog-<platform>-notes@v2命名空间的包缓存(release-notes.ts),缓存时长由releaseNotesCacheMinutes决定:发布不足一周缓存 55 分钟、不足半年约 1 天、更久约 10 天。

3.2 GitLab 侧的文件定位

对于 GitLab 仓库,gitlab/index.ts 的getReleaseNotesMd按以下顺序工作:

  1. 调用GET /projects/<url编码仓库>/repository/tree?per_page=100(可选&path=<sourceDirectory>)列出仓库树,并开启分页(paginate: true);
  2. 从返回的 tree 节点中过滤出type === 'blob'的文件;
  3. changelog-filename-regex正则匹配文件名,筛出形如CHANGELOG.mdCHANGELOGCHANGELOG.json的候选;
  4. 若有多个候选,用 common.ts 的compareChangelogFilePath排序:优先.md/.markdown/.mkd,其次.txt/.text,其余类型垫底——这正是为了避免出现CHANGELOG.json而错过CHANGELOG.md的问题;
  5. 取排序后第一个文件,调用GET /projects/<仓库>/repository/blobs/<blob_id>/raw拉取原始文本;
  6. 在文末拼接\n#\n##哨兵后返回{ changelogFile, changelogMd }

adapter-utils.md这个 fixture 的取值场景,正是模拟上述第 5 步拿到的原始内容。

四、核心解析:sectionize 切分、标题匹配与正文提取

这是整条管线的灵魂,实现在 release-notes.ts 的getReleaseNotesMd中。

4.1 按标题级别切分:sectionize

function sectionize(text: string, level: number): string[] { const tokens = markdown.parse(text, {}); tokens.forEach((token) => { if (token.type === 'heading_open') { const lev = +token.tag.substring(1); if (lev <= level) { sections.push([lev, token.map![0]]); } } }); sections.push([-1, lines.length]); // 取出所有恰好等于 level 的节 }

解析器使用markdown-it(仅启用headinglheadingfence三个规则,见 release-notes.ts)把整个 changelog 文本 token 化,然后从 level 1 到 level 7 依次尝试:只要某个级别能切出至少 2 节,就按该级别遍历每个 section。这样无论样本用的是#(如# Current Version: 4.3.1)还是##(如## 4.33.0 [05-15-2020]),都能被正确切分。

adapter-utils.md而言,顶层# Current Version: 4.3.1# Previous Version: 1.3.2构成 level 1 的两节;而平铺段的## 4.33.0 [05-15-2020]系列则会在 level 2 被切出。\n#\n##哨兵保证了最后一个版本节(4.24.0)之后有更低级标题用于闭合。

4.2 标题内版本匹配:Look for version in title

对每个 section,解析器做三件事:

const deParenthesizedSection = section.replace(regEx(/[[\]()]/g), ' '); const [heading] = deParenthesizedSection.split(newlineRegex); const title = heading.replace(regEx(/^\s*#*\s*/), '').split(' ').filter(isTruthy); const body = section.replace(regEx(/.*?\n(?:-{3,}\n)?/), '').trim();
  • 去括号化:把所有[]()替换为空格,避免链接语法干扰分词;
  • 取标题分词:剥掉开头的#,按空格拆词;
  • 去正文前缀:用.*?\n(?:-{3,}\n)?把标题行及紧随其后的---分隔线从正文中剥离。

随后遍历标题中的每个词,只要某个词包含目标版本号且不是 URL,即命中:

if (word.includes(version) && !isHttpUrl(word)) { return { body: await linkifyBody(project, body), url, notesSourceUrl }; }

## 4.33.0 [05-15-2020]而言,标题分词后是['4.33.0', '[05-15-2020]'](去括号后),目标版本4.33.0直接命中,正文即* add new auth, fix accept header and base path in mock\n\nCloses ADAPT-207\n\nSee merge request itentialopensource/adapter-utils!177

4.3 正文内版本匹配:monorepo 与内联版本场景

fixture 中的旧版格式(__3.10.0 [12-05-2018]__内联加粗、# Current Version: 4.3.1这类不含日期版本对)无法靠标题命中。解析器为此实现了第二套策略(release-notes.ts):

  1. 标题中必须含YYYY-MM-DD日期格式(releasesRegex),规避没有日期的普通标题;
  2. 正文中必须同时出现packageName与目标version(适用于 monorepo 中包名与版本散落在条目里的情况);
  3. 逐行检查时跳过 Markdown 链接引用定义[1.2.3]: https://…形式,即 Keep-a-Changelog 文末常见的 compare 链接表),否则每一行都会"伪命中";
  4. 同样排除含 URL 的行。

测试用例parses when version contained in the body 0.14.0ignores trailing link reference definitions when searching body(见 release-notes.spec.ts)正是为验证这套策略而设。

五、输出后处理:massageBody、linkify 与 URL 生成

命中后,返回结构遵循 types.ts 中的ChangeLogNotes接口:bodyurlnotesSourceUrl。其中:

  • notesSourceUrlsource.getNotesSourceUrl(baseUrl, repository, changelogFile)生成。对 GitLab,gitlab/source.ts 的GitLabChangeLogSourcegitlab-tags为 datasource,getAPIBaseUrl返回<baseUrl>api/v4/,最终拼接出如<baseUrl>itentialopensource/adapter-utils/blob/HEAD/CHANGELOG.md的源文件地址;
  • url是锚点链接,由getReleaseNotesMdAnchorUrl(notesSourceUrl, parenthesizedHeading)对原始标题(保留括号)做 slug 化生成,如#4330-05-15-2020。测试中对4.33.0的断言正是…/CHANGELOG.md#4330-05-15-2020
  • body经过 massageBody 清洗:统一\r\n、剔除 semantic-release 的<a name>行与 compare 链接、#/##/####逐级降为###/####/#####(代码块内的#会被保护不处理),最后 trim 空白。

最后linkifyBody会把正文中的仓库引用(如itentialopensource/adapter-utils!177这类 merge request 引用)转成可点击链接。在getReleaseNotes路径(GitLab release API 而非 MD 文件)中,releaseNotesResult会为 GitLab 拼接<baseUrl><repository>/tags/<tag>作为 URL,并对非https://gitlab.com/的实例执行同样的 linkify(release-notes.ts)。

六、测试如何验证:release-notes.spec.ts 中的适配器样本用例

fixture 的最终价值落在测试上。在 release-notes.spec.ts 的parses adapter-utils 4.33.0用例中:

  1. httpMock模拟 GitLab 的两个接口:GET /api/v4/projects/itentialopensource%2Fadapter-utils/repository/tree?per_page=100返回gitlabTreeResponseGET …/repository/blobs/abcd/raw返回本 fixture 的内容;
  2. repository: 'itentialopensource/adapter-utils'version: '4.33.0'gitRef: '4.33.0'调用getReleaseNotesMd
  3. 断言结果:
    • notesSourceUrl指向…/blob/HEAD/CHANGELOG.mdurl#4330-05-15-2020锚点;
    • 正文以- add new auth, fix accept header and base path in mock\n开头(注意:massageBody的降级规则把原文档中的## 4.33.0节标题降为####后从正文剥离,正文从第一条*条目开始,且*linkify处理为-列表项);
    • 正文包含Closes ADAPT-207See merge request itentialopensource/adapter-utils!177
    • 正文以***结尾
    • 相邻版本不泄漏:正文既不包含ADAPT-198,也不包含set username and password in token entitypath——这正是sectionize精确切分的直接验证。

同文件还包含handles gitlab sourceDirectory用例(release-notes.spec.ts):把 tree 响应中的每个文件路径加上packages/foo/前缀后,同一份 fixture 应被解析出notesSourceUrl指向…/blob/HEAD/packages/foo/CHANGELOG.md,验证了sourceDirectory场景下"同一 changelog 内容、不同来源 URL"的正确性。

七、更完整的调用链:从版本列表到 PR 正文

将上述环节串起来,完整的调用链是:

  1. addReleaseNotes(release-notes.ts)遍历每个待展示的版本;
  2. 先查包缓存,未命中则依次尝试getReleaseNotesMd(解析 CHANGELOG 文件)→getReleaseNotes(GitLab/GitHub release API)→ 退化为 compare URL;
  3. 缓存时长按版本发布日期动态决定;
  4. config.fetchChangeLogs === 'pr'时,会受平台 PR 正文长度上限约束:累计正文达到platform.maxBodyLength()后停止继续抓取更旧版本(因为按从新到旧顺序,最新变更始终可见)。

adapter-utils.md作为 fixture,精确覆盖了第 2 步中"GitLab + 头部平铺格式 + 旧版分类格式 + 文末哨兵 + 合并请求引用"这一组合,是 Renovate 保证不同格式 changelog 都能被正确解析的基石性测试样本。

八、小结与延伸阅读

围绕 adapter-utils.md 这 1000 余行的真实 changelog 样本,可以总结出 Renovate 变更日志解析的三条设计原则:

  1. 格式无关:通过 1~7 级标题逐级探测切分,兼容 Keep-a-Changelog、逐版本平铺、内联加粗版本等异构风格;
  2. 精确不泄漏sectionize+ 哨兵标题 + 链接引用定义过滤,确保只提取目标版本、不污染相邻版本;
  3. 真实样本驱动:测试直接用真实项目的 changelog 全文,让解析器始终面对"现实世界的脏数据"。

如果你希望进一步深入,可以从以下文件继续阅读:

  • 解析与清洗主逻辑:release-notes.ts
  • GitLab 侧文件定位与 release 列表:gitlab/index.ts
  • changelog 文件优先级排序:common.ts
  • 数据结构定义:types.ts
  • 覆盖 adapter-utils 样本的测试用例:release-notes.spec.ts
  • 其他同类测试样本:jest.mdyargs.mdjs-yaml.mdgitter-webapp.mdangular-js.md(均在fixtures目录下)

【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询