- 医疗健康
- 后端
【免费下载链接】openemr
The most popular open source electronic health records and medical practice management solution.
导读
本文围绕 OpenEMR 8.2.0 的官方变更日志(CHANGELOG)展开,以仓库中锁定的 8.2.0 发布期变更日志快照 tests/Tests/Isolated/Release/fixtures/8_2_0/release-time/expected.md 为骨架,结合其背后真正生成这份文档的自动化工具链 ——ChangelogGenerator及其回归测试、JSON 测试夹具,还原 OpenEMR 是如何从一次真实的 Git 提交区间(v8_0_0 → v8_2_0,约 650 个 PR)自动产出结构化、带分类、带安全公告(GHSA)关联的发布说明。读完本文,你将掌握这套变更日志的产出格式约定、分类规则、安全公告匹配逻辑,以及如何借助测试夹具与 CLI 命令在本地复现和验证同样的生成过程。
一、8.2.0 变更日志的整体面貌
1.1 文档的定位:一份"被锁定"的生成器输出
expected.md不是一份手写的发布公告,而是 OpenEMR 发布自动化中一个回归测试的期望输出。它存放在tests/Tests/Isolated/Release/fixtures/8_2_0/release-time/目录下,与同目录的advisories.json、父目录的commits.json、prs.json一起构成一组"真实输入 + 期望输出"的夹具,专门用于回放 8.2.0 的真实发布输入,验证ChangelogGenerator在**发布时刻(release-time)**场景下的渲染结果与当时实际发布的 CHANGELOG 完全一致。
该文档的正文第一行是标准的版本条目:
## [8.2.0](https://github.com/openemr/openemr/compare/v8_0_0...v8_2_0) - 2026-07-08这一行本身包含三条关键信息,分别由生成器的三个独立环节决定:
- 版本号
8.2.0:来自调用方的--title参数(即目标发布版本); - 比较链接
v8_0_0...v8_2_0:base取上一个已发布版本v8_0_0(而非被切出但从未发布的 v8_1_0),head取v8_2_0,详见 ChangelogGenerator.php 的 compare URL 构造逻辑; - 日期
2026-07-08:8.2.0 标签的创建日期。测试中通过冻结时钟FrozenClock('2026-07-08T00:00:00+0000')保证渲染日期确定。
1.2 正文的组织结构
整份 8.2.0 变更日志按以下层次组织:
| 层级 | 示例 | 说明 |
|---|---|---|
| 版本标题(H2) | ## 8.2.0 - 2026-07-08 | 版本号、比较链接、发布日期 |
| 安全修复(H3) | ### Security Fixes | 仅在命中 GHSA 时出现 |
| 一级分类(H3) | ### Fixed/### Added/### Changed | 由 Conventional Commits 前缀映射而来 |
| 功能域子分组(H4) | #### Authentication、#### Security、#### PHP | 取自 PR 的功能标签(area label) |
| 条目(列表项) | - add NPI to user ... (#11916) | 每条一个 PR |
该文档的### Fixed分类下包含约 60 个功能域子分组(Authentication、Backend Modernization Project、CCDA Service、Calendar、Clinical Decision Support、Database Layer、Database Migrations & Schema Changes、DevOps、Documentation、EHI Export、Hardening、Infrastructure、Internationalization、Labs、Module Support、Ophthalmology、PHP、Patient Portal、REST API、Reports、Security、UI Modernization、UI/UX、billing & payments、code set update、communications、docker、e-Prescribe、encounter、installer、javascript、multi-site、patient admin、practice settings、selenium、testing、translation 等),### Added与### Changed同样按此规则分组。
二、这份变更日志是如何生成的:ChangelogGenerator 核心链路
2.1 主流程:从提交区间到分类条目
生成器的入口是ChangelogGenerator::generate()(tools/release/src/ChangelogGenerator.php),其执行链路为:
- 枚举提交:调用
$this->api->commitsBetweenRefs($base, $head)获取两个 ref 之间的全部提交 SHA; - 解析 PR:调用
$this->api->prsForCommits($shas)将提交解析回合并的 PR,再经filterNoise()剔除噪声 PR; - 分类:对每个 PR 调用
categorize(),解析 Conventional Commits 前缀、提取功能域标签、判定是否属于开发者变更; - 排序:按 PR 标题做不区分大小写的字典序排序(
strcasecmp),保证输出稳定、可复现; - 分区:将 PR 拆分为标准条目(
is_dev=false)与开发者条目(is_dev=true); - 匹配安全公告:若
includeGhsa为真,则调用matchAdvisories(),将发布区间内的提交 SHA / PR 号与已发布的 GHSA 做关联; - 渲染:依次输出标题行、安全修复节、标准 PR 节(Fixed → Added → Changed → Dependencies 顺序,深度 3),最后是"OpenEMR Developer Changes"开发者节(深度 4)。
2.2 分类规则:Conventional Commits 前缀映射
每个 PR 标题会先经过CC_PATTERN正则匹配(ChangelogGenerator.php):
private const CC_PATTERN = '/^(feat|fix|deps|chore|refactor|docs|perf|test|ci|build|style)(\(.+?\))?!?:\s*(.+)$/i';匹配成功后按CATEGORY_MAP映射为一级分类,其余落入默认分类Changed(ChangelogGenerator.php):
private const CATEGORY_MAP = [ 'feat' => 'Added', 'fix' => 'Fixed', 'deps' => 'Dependencies', ]; private const SECTION_ORDER = ['Fixed', 'Added', 'Changed', 'Dependencies']; private const DEFAULT_CATEGORY = 'Changed';feat:→### Addedfix:→### Fixeddeps:→### Dependencies(该 8.2.0 快照中无此分类条目)- 其余前缀(
chore、refactor、docs、perf、test、ci、build、style)或非约定式标题 →### Changed
注意:8.2.0 的 expected.md 中仅出现Fixed与Added两类,且 Fixed 条目数量远大于 Added,这与一个 8.0.0 → 8.2.0 跨度达 650 个 PR、以缺陷修复为主的真实发布区间相吻合。
2.3 功能域分组:标签驱动
PR 的第二个标签(第一个非跳过标签)被用作功能域(area)。SKIP_LABELS定义了约 30 个不构成功能域的流程/元标签(backport、bleeding、Stale、Status: Needs Review、dependencies、github-actions 等),见 ChangelogGenerator.php。因此 8.2.0 中如#### Security、#### PHP、#### REST API等子分组,全部来自各 PR 的真实 GitHub 标签名。
开发者变更标记同样由标签驱动:PR 若带developers标签,则is_dev=true,会被收进文档末尾的### OpenEMR Developer Changes一节(该 8.2.0 快照中此节未出现,说明该区间内无带developers标签的 PR)。
2.4 噪声过滤:谁不会出现在变更日志里
filterNoise()/isNoise()(ChangelogGenerator.php)负责剔除三类 PR:
- 发布机器人:作者为
openemr-release-bot[bot]的 PR(版本号提升、建标签、同步 PR); - 测试标记:标题含
[TEST]的 PR; - 发布切割提交:匹配
^chore(?:\([^)]*\))?:\s*release\s+v?\d的手工发布 PR。
对 Dependabot PR 还有两重专门规则:
isNoOpVersionBump():bump <dep> from <v> to <v>且前后版本相同(无实际变化的重新固定版本)会被剔除;isDockerBump():标题含in /docker/...、in /ci/...路径信号,或命中DEPENDABOT_DOCKER_GROUPS(couchdb、mariadb、mysql、redis、selenium 等 10 个 docker-compose 分组名)的分组式升级会被剔除。
dependabot 的 composer/npm 依赖升级则保留,因为那是真实的、面向用户的依赖变更——8.2.0 的#### PHP子分组中大量bump phpunit/phpunit、bump symfony、bump guzzlehttp/psr7条目正是这一规则的直接体现。
2.5 安全公告(GHSA)关联逻辑
matchAdvisories()(ChangelogGenerator.php)遍历仓库所有已发布 GHSA,通过advisoryMatchesRange()(L446-L481)判断是否属于本发布:
- 主信号——Patched versions 精确匹配:GHSA 的
vulnerabilities[].patched_versions恰好等于目标发布版本串(如"8.2.0")。这是 OpenEMR 的约定做法(见 RELEASE_PROCESS.md),也是实践中主要命中路径; - 次信号——引用匹配:GHSA 的 References 中出现区间内的 40 位提交 SHA(
/commit/{40hex})或 PR 号(/pull/{N})链接。
命中后按严重级别(critical → high → medium → low)排序渲染为:
### Security Fixes - [High] OpenEMR FaxSMS module: insecure staging of decrypted patient documents in webroot (CWE-552/CWE-200) ([GHSA-vv5j-6gjw-ffx9](https://github.com/openemr/openemr/security/advisories/GHSA-vv5j-6gjw-ffx9))2.6 渲染安全:Markdown 转义与 URL 白名单
escapeMarkdown()对 PR 标题、区域标签、公告摘要中的[和]做转义,防止贡献者可控文本注入 Markdown 链接语法;sanitizeGitHubUrl()只放行https://github.com/openemr/开头的 URL,其余一律替换为中性占位链接,杜绝变更日志"夹带"站外链接。
三、release-time 与 post-ghsa:同一发布的两种发布时相
夹具目录刻意提供两个孪生场景,模拟同一发布的两个时间点,这正是该测试最有价值的地方:
| 场景 | 目录 | advisories.json | 渲染差异 |
|---|---|---|---|
| 发布时刻 | fixtures/8_2_0/release-time/ | [](空,该发布的 GHSA 尚在草稿) | 无### Security Fixes节 |
| 公告发布后 | fixtures/8_2_0/post-ghsa/ | 1 条已发布 GHSA(patched_versions = "8.2.0") | 顶部插入### Security Fixes节,列出 GHSA-vv5j-6gjw-ffx9 |
两份 expected.md 除第一行标题相同外,release-time版本直接进入### Fixed,而post-ghsa版本在标题后多出安全修复节。这模拟了 OpenEMR 的发布流程:发布切割时公告未公开(或未标 patched_versions),安全节为空;待公告正式发布并回填Patched versions后,通过修订派发重新生成补丁版变更日志。
四、测试验证:如何证明生成器与文档一致
4.1 回归测试的组织方式
ChangelogGeneratorFixtureTest(tests/Tests/Isolated/Release/ChangelogGeneratorFixtureTest.php)是这份 expected.md 的"主人":
public function testRegeneratesEightPointTwoZeroAtReleaseTime(): void { $this->assertScenarioRendersExpected('release-time'); } public function testRegeneratesEightPointTwoZeroPostGhsaAmendment(): void { $this->assertScenarioRendersExpected('post-ghsa'); }两个测试共享父目录的commits.json(约 650 个提交 SHA)与prs.json(对应的 PR 元数据),仅advisories.json与expected.md不同。测试通过FakeGitHubApi注入真实输入,用FrozenClock冻结在 2026-07-08(8.2.0 标签日期),调用:
$generator = new ChangelogGenerator($api, 'openemr/openemr', $clock); $actual = $generator->generate('v8_0_0', 'v8_2_0', '8.2.0', includeGhsa: true);然后断言$actual与expected.md逐字节一致。
4.2 夹具更新协议:UPDATE_FIXTURE=1
当生成器的过滤、分类、分区顺序或公告渲染规则发生有意变更时,用环境变量更新期望输出:
UPDATE_FIXTURE=1 vendor/bin/phpunit tests/Tests/Isolated/Release/ChangelogGeneratorFixtureTest.php此时测试不再断言,而是把当前输出覆写回 expected.md,并跳过(skip)。开发者需人工审查 diff 后,将代码变更与期望文件一并提交;下次无环境变量运行时恢复断言。这保证了仓库内这份 8.2.0 变更日志始终与生成器行为同步,不会悄悄漂移。
4.3 与单元测试的分工
ChangelogGeneratorTest(tests/Tests/Isolated/Release/ChangelogGeneratorTest.php)用合成 PR 形状逐个覆盖过滤/分类分支,定位回归精确到方法级别、速度快;而夹具测试回放真实 ~650 PR 区间,捕捉过滤 + 分类 + 区域分组 + 公告匹配之间的组合性交互。两者互补,前者"找局部",后者"验整体"。
五、在本地复现生成
5.1 前提条件
OpenEMR\Release\命名空间下的类位于autoload-dev,因此需要包含开发依赖的 composer 安装;否则 CLI 会直接报错退出(exit 2):
composer install5.2 CLI 命令
tools/release/bin/changelog.php(tools/release/bin/changelog.php)是生成器的命令行封装,基于 Symfony Console 的SingleCommandApplication:
# 输出到 stdout php tools/release/bin/changelog.php --base v8_0_0 --head v8_2_0 --title 8.2.0 # 禁用安全公告节(模拟 release-time 空公告) php tools/release/bin/changelog.php -b v8_0_0 -t 8.2.0 --no-ghsa # 写入文件 php tools/release/bin/changelog.php -b v8_0_0 --head v8_2_0 -t 8.2.0 -o changelog/8.2.0.md可用参数:
| 选项 | 短选项 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
--base | -b | 是 | 无 | 基准 ref(上一个发布标签) |
--head | 无 | 否 | HEAD | 头部 ref(标签或分支) |
--title | -t | 否 | 无 | 标题中的版本串,省略则仅输出正文 |
--no-ghsa | 无 | 否 | 启用 | 禁用安全公告节 |
--repo | -r | 否 | openemr/openemr | GitHub 仓库(owner/name) |
--output | -o | 否 | stdout | 输出文件路径 |
--head与--title的配合也体现了 ChangelogMutator 的用法:compareLinkOverride允许在目标标签尚不存在时,用vPREV...vNEW的"理想标签对"渲染比较链接,而实际提交区间仍从已存在的 rel 分支枚举——这正是发布准备(release-prep)阶段生成"前瞻性"变更日志的方式。
5.3 夹具捕获脚本
真实输入夹具通过 tools/release/bin/capture-changelog-fixture.php 从实时 GitHub API 状态捕获;公告在捕获时即按生成器会渲染的条件(patched_versions 精确匹配目标版本,或 SHA/PR 引用匹配)过滤,从而保证夹具不会因上游无关 GHSA 的发布而失稳。
六、与仓库其他发布自动化模块的衔接
这份 expected.md 处于发布自动化链条的中游:其输入的 prev_release 推导、版本映射分别由 BranchVersionResolver(决定 base 取 v8_0_0 而非 v8_1_0,因为后者"被切出但从未发布")与 tools/release/bin/derive-prev-release.php、tools/release/bin/branch-to-version.php 承担;生成结果则由 ChangelogMutator 写回仓库的 CHANGELOG.md。围绕它展开的还有发布文档 docs/RELEASE_PROCESS.md 与发布自动化规划 docs/release-automation-plan.md。整个tests/Tests/Isolated/Release/目录下 30 余个测试类(TagCreator、ShipReleaseOrchestrator、PackageAssembler、DispatchCli 等)共同支撑了 OpenEMR 的自动化发布管线,而 8.2.0 的 expected.md 正是这条管线在"变更日志生成"环节留下的最完整、最真实的快照证据。
结语
release-time/expected.md表面是一份常规的版本变更日志,实际是 OpenEMR 发布自动化体系中一枚被测试锁定的"金标本":它以 v8_0_0 → v8_2_0 约 650 个 PR 的真实数据,完整记录了 Conventional Commits 分类、标签驱动的功能域分组、机器人/依赖噪声过滤、GHSA 安全公告关联、Markdown 渲染安全等全部规则在真实输入下的输出形态。无论是希望理解 OpenEMR 的发布流程,还是想为自己的项目搭建"自动生成、测试锁定、可复现"的变更日志管线,这份文档与其背后的生成器、测试和 CLI 都是可以直接借鉴的完整范例。
- 医疗健康
- 后端
【免费下载链接】openemr
The most popular open source electronic health records and medical practice management solution.
相关推荐
Humanizer 流式日期 API 详解:OnDate.March 类源码、生成机制与测试验证
Humanizer 流式日期 API 详解:OnDate.March 类源码、生成机制与测试验证 本文以 Humanizer 仓库中 Humanizer.OnD
开发工具Renovate 如何解析 CHANGELOG:以 yargs 变更日志为样例的源码级剖析
Renovate 如何解析 CHANGELOG:以 yargs 变更日志为样例的源码级剖析 Renovate(Mend.io 出品的跨平台依赖自动化工具)在创建
开发工具DevOps后端QQ空间说说导出指南:扫码一次,把全部历史说说免费存进本地
QQ空间说说导出指南:扫码一次,把全部历史说说免费存进本地 GetQzonehistory 是一个开源的 QQ 空间说说导出工具:手机 QQ 扫个码登录,它就能
网页爬虫数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考