PMD 发布与版本策略全解析:Release Schedule、SemVer 兼容性承诺与 Git 分支/标签政策

原创2026-10-11 23:47:251,985 阅读
文章标签:静态分析代码质量

PMD 发布与版本策略全解析:Release Schedule、SemVer 兼容性承诺与 Git 分支/标签政策

PMD 作为一个多语言静态代码分析工具,其版本演进直接关系到你 CI/CD 流水线的稳定性与升级成本。本文以仓库官方文档 release_policies.md 为骨架,结合仓库源码、发布脚本与配套策略文档,系统讲解 PMD 的时间驱动发布节奏、MAJOR/MINOR/PATCH 三级版本各自的兼容性承诺,以及 Git 分支与标签的命名规则。读完本文,你将能准确判断"某个版本能不能直接替换升级""升级后构建为什么可能突然失败""补丁版本该从哪里打分支"这类关键问题。

发布时间表:时间驱动的发布节奏

PMD 采用基于时间的发布排期(time-based release schedule),而不是"功能攒够了再发"的发布模式:

  • 每月的最后一个周五发布一个新的 minor(次版本)版本;
  • patch(补丁)版本按需发布,只有当确实有必要(例如出现 blocker 级别的严重 bug)时才会发布。

这一节奏意味着 PMD 的演进是可预期的:你可以在日历上规划升级窗口,而不是被动等待大版本发布。从仓库根目录的 pom.xml 可以看到当前开发版本为 7.29.0-SNAPSHOT,与 docs/_config.yml 中的 version: 7.29.0-SNAPSHOT 保持一致;发布脚本 do-release.sh 也据此在 MAJOR.MINOR.PATCH-SNAPSHOT 与正式版本号之间来回切换(见下文"发布流程"一节)。

版本号政策:遵循 SemVer 的 MAJOR.MINOR.PATCH

PMD 的目标是遵循 Semantic Versioning(语义化版本),即版本号形式为 MAJOR.MINOR.PATCH。三个数字分别承载不同等级的兼容性承诺,理解这三者的差异是评估升级风险的前提。

MAJOR:可以破坏任何兼容性

major(主版本)发布允许破坏任何兼容性,因此升级到下一个主版本通常意味着更多的工作量——正如从 6.x 升级到 7.x 那样。仓库中为此专门维护了一份完整的迁移指南:migrating_to_pmd7.md。该指南的"Before you update"章节建议:

  • 先升级到 PMD 6 的最终版本 6.55.0,并尽力清除所有 deprecation 警告;
  • 注意废弃的 CLI 参数(如 -no-cache → --no-cache、-reportfile → --report-file、-language → --use-version);
  • 使用旧式 ruleset 引用(如 java-basic、600)会输出类似 Ruleset reference 'java-basic' uses a deprecated form, use 'rulesets/java/basic.xml' instead 的警告,这些旧写法在 PMD 7 中已不再兼容。

主版本升级是"一次性大手术",需要预留迁移时间。

MINOR:努力做到"即插即用",但存在例外

minor(次版本)发布尝试保持兼容,使得"零成本替换(drop-in replacement)"式的升级成为可能——但存在一些例外。minor 版本可能包含以下内容:

内容 对构建的影响
修复规则的误报(FP,false-positive) 通常更友好,违规数量可能减少
修复规则的漏报(FN,false-negative) 可能让构建失败:修复后会发现新的违规
新增规则(new rules) 如果你使用自定义 ruleset,新规则默认不启用,一般不影响构建;但如果你直接引用整个 category,新规则会被纳入
废弃已有功能(deprecations) 不会立刻移除,但会产生警告

文档给出的总结非常直白:PMD 只对"你如何集成/使用这个工具"这一层面保证稳定性,但构建仍可能因为 FN 修复或引入新规则而开始失败(尤其是对于那些直接引用整个 category 的用户)。在移动/重命名规则时,PMD 坚持"先废弃、再引用新规则、直到下一个主版本才移除"的现行做法,这部分细节可参见仓库的 rule_deprecation.md。

PATCH:绝对的即插即用

patch(补丁)版本绝对地是 drop-in replacement,它只包含:

  • 纯 bug 修复:例如崩溃、明显坏掉的功能(如规则完全没有被应用);
  • 安全问题修复:依赖升级、加固(hardening)等。

补丁版本不会新增规则、不会改变规则行为、不会引入任何会影响构建结果的变更,因此在 7.15.0 这类次版本之下的 7.15.1、7.15.2 可以放心无脑升级。

从 docs/_config.yml 可以看到,PMD 用 release_type 字段标记每次发布的类型,注释中写明取值只有三种:

pmd:
    version: 7.29.0-SNAPSHOT
    previous_version: 7.28.0
    date: 2026-10-30
    # release types: major, minor, bugfix
    release_type: minor

bugfix 对应上述 PATCH 级补丁(如 7.15.x)、minor 对应次版本(7.x.0)、major 对应主版本(x.0.0)。发布脚本 do-release.sh 中的版本号推导逻辑也与此呼应:当 PATCH == 0 时按 minor 处理,下一个开发版本是 MINOR+1.0-SNAPSHOT;当 PATCH != 0 时按 bugfix 处理,下一个开发版本是 MINOR.PATCH+1-SNAPSHOT。

版本政策背后的源码级支撑

版本承诺不是一句口号,而是由仓库中的架构决策记录(ADR)、废弃策略文档和真实注解代码共同落实的。

ADR 3:API 演化原则

release_policies.md 中"See also"第一个指向的就是 adr-3.md。这份已被接受(Accepted)的架构决策记录,把"版本号如何增长"细化到了代码层面:

  • 语义化版本落实到 API:MAJOR 用于不兼容的 API 变更,MINOR 用于向后兼容的新功能,PATCH 用于向后兼容的 bug 修复;
  • 包结构规约:所有源码基包为 net.sourceforge.pmd,语言模块使用 net.sourceforge.pmd.lang.<language id>(如 pmd-java 对应 net.sourceforge.pmd.lang.java),其他模块使用 net.sourceforge.pmd.<module>;
  • public / internal / impl 三级边界:
    • 默认情况下所有包都被视为 public API,但包含 internal 段的包(如 net.sourceforge.pmd.internal)是内部 API,可以随时以任何方式修改甚至移除,且不需要 MAJOR 版本变更;
    • 包含 impl 段的包(如 net.sourceforge.pmd.lang.impl)也被明确视为内部实现,是"扩展 PMD(如新增语言)"所需的基类所在,可随时变更;
    • 落在 public 包中但不应对外使用的成员,用 @InternalApi 注解标记,并附加 javadoc 标签 @internalApi None of this is published API, and compatibility can be broken anytime! Use this only at your own risk.;
  • 废弃与移除节奏:API 可以在任何时候(甚至 PATCH 版本中)通过 @Deprecated 注解标记废弃,废弃项必须记录在 release notes 中,但只能通过 MAJOR 版本变更才能移除;
  • 实验性 API:新特性引入的新 API 可以用 @Experimental 标记,它们被认为不稳定,可以在任何时候被修改或移除,不应在生产代码中依赖;实验性 API 的引入/移除/转正至少需要 MINOR 版本变更。

这些注解的源码定义可以直接在仓库中查看:InternalApi.java 与 Experimental.java,它们都位于 pmd-core 模块的 net.sourceforge.pmd.annotation 包中。这与"minor 版本可能包含 deprecations""移除必须等 major"的版本承诺是同一套逻辑的两面。

规则废弃策略:从重命名到最终移除的完整生命周期

release_policies.md 的第二个 "See also" 指向 rule_deprecation.md,它详细规定了规则的废弃与移除规程:

  • 重命名规则:先改名为新名称(连同测试和资源),再在 ruleset 中添加一条指向新规则、带 deprecated="true" 的旧引用:

    <rule name="OldRuleName" ref="NewRuleName" deprecated="true" />
    
  • 在 category 之间移动规则:在旧 category 中添加指向新位置的废弃引用:

    <rule name="MyRule" ref="category/java/errorprone.xml/MyRule" deprecated="true" />
    

    注意:当完整引用 ruleset 或 category 时,这些废弃引用会被忽略,从而避免规则被重复使用(例如两个 category 都被引用时)。

  • 废弃规则:在规则定义上打上 deprecated="true",效果是在完整引用该 ruleset/category 时该规则被自动禁用,但直接引用时仍可使用;废弃原因应在规则描述中说明,如有替代规则也应一并注明;

  • 移除规则:只有当规则先前已废弃,且是在新的主版本发布时,才能彻底移除——因为直接移除会破坏所有引用该规则的自定义 ruleset;

  • 规则属性兼容性:重命名/移除规则属性属于不向后兼容的变更,只能随主版本发布进行;属性可以先行废弃(描述以魔术字符串 deprecated! 开头,会在规则文档中渲染为废弃状态),实现上需要先检查旧属性、再检查新属性并输出废弃警告;修改属性默认值可能导致行为不兼容(例如默认配置下违规数量激增),应逐例评估,若不兼容则只能随下一个主版本生效。

仓库中的测试资源可以验证这套机制确实在运行,例如 pmd-core/src/test/resources/rulesets/dummy/basic.xml 中就同时存在"旧名称指向新规则"的废弃引用(<rule deprecated="true" name="OldNameOfDummyBasicMockRule" ref="DummyBasicMockRule"/>)与真正被打上 deprecated="true" 的规则,而 pmd-apex/src/main/resources/category/apex/design.xml 等真实 ruleset 中也可见多个带 deprecated="true" 的规则条目。

从源码结构看版本承诺的落地

从源码结构看,PMD 的版本策略与支持生命周期是联动的:仓库 support_lifecycle.md 中的支持矩阵显示,只有最新主版本(当前为 7.x)处于活跃开发并持续获得新特性与 bug 修复,一旦新主版本发布,前一个主版本即变为不受支持——这正是"兼容性承诺只保证到下一个 major 为止"在生命周期维度上的体现。当前 7.x 要求 Java 8 即可运行,而构建 PMD 本身需要 Java 21(见 docs/_config.yml 的 java_build_version: 21、java_run_version: 8)。

升级影响评估:什么时候构建会突然失败

综合上述政策,可以给出一个实用的风险判断表:

  • PATCH 升级:无风险,绝对即插即用,只包含 bug 与安全修复;
  • MINOR 升级:基本即插即用,但存在两类"例外"可能让构建失败——修复 FN 后发现新违规、以及直接引用整个 category 时纳入新规则;此外废弃功能会产生警告,需要留意但不阻塞;
  • MAJOR 升级:允许破坏一切兼容性,废弃 API/规则/属性可能被移除,必须走迁移指南。

升级到主版本前,官方建议先升到前一系列的最新版(如 6.55.0)并清理所有废弃警告,再逐步经过 release candidate 版本过渡到正式版,具体步骤见 migrating_to_pmd7.md。

Git 分支与标签政策

PMD 的 Git 分支与标签策略相当精简,核心要点如下:

  • 主开发在 main 分支上进行(当前分支名就是 main);
  • 所有 PR 与功能增强都直接合入 main 分支;
  • 发布通常直接基于 main 分支进行,不创建独立的 release 分支;
  • 每个发布都有一个以 pmd_releases/MAJOR.MINOR.PATCH 命名的标签;
  • patch 发布的分支策略:
    • 如果 main 上没有正在进行的开发,则直接从 main 分支发布;
    • 否则,从上一个发布标签切出独立分支来做补丁发布。

配套的发布流程详见仓库的 releasing.md。从该文档可以看到标签命名的实际使用方式,例如发布 7.15.0 时执行的命令序列:

RELEASE_VERSION=7.15.0
DEVELOPMENT_VERSION=7.16.0-SNAPSHOT
./mvnw --quiet versions:set -DnewVersion="${RELEASE_VERSION}" -DgenerateBackupPoms=false -DupdateBuildOutputTimestampPolicy=always
sed -i "s|<tag>.\+</tag>|<tag>pmd_releases/${RELEASE_VERSION}</tag>|" pom.xml
./mvnw clean verify -Pgenerate-rule-docs
git commit -a -m "[release] prepare release pmd_releases/${RELEASE_VERSION}"
git tag -m "[release] copy for tag pmd_releases/${RELEASE_VERSION}" "pmd_releases/${RELEASE_VERSION}"
./mvnw --quiet versions:set -DnewVersion="${DEVELOPMENT_VERSION}" -DgenerateBackupPoms=false -DupdateBuildOutputTimestampPolicy=never
sed -i "s|<tag>.\+</tag>|<tag>HEAD</tag>|" pom.xml
git commit -a -m "[release] prepare for next development iteration"
git push origin "${CURRENT_BRANCH}"
git push origin tag "pmd_releases/${RELEASE_VERSION}"

其步骤是:把 POM 版本从 -SNAPSHOT 改为发布版本 → 本地跑通 clean verify → 提交并打 pmd_releases/<版本> 标签 → 把版本改回下一个 -SNAPSHOT → 推送分支与标签。标签推送后,GitHub Actions 的 "Build Release" 与 "Publish Release" 工作流会自动接管发布(详见 github_actions_workflows.md),完成 Maven Central 部署、文档与 javadoc 上传、GitHub Release 创建、Docker 镜像构建等一系列动作。

这些流程也通过根目录的 do-release.sh 脚本半自动化:脚本会校验当前目录、Ruby/bundler 版本,自动推导 RELEASE_VERSION、DEVELOPMENT_VERSION、LAST_VERSION(PATCH == 0 时按 minor 推导下一个版本,否则按 bugfix 推导),并串联 release notes 生成、Jekyll 文档渲染、版本号切换与标签创建。标签名 pmd_releases/<版本> 贯穿脚本全程,例如 git tag -m "[release] copy for tag pmd_releases/${RELEASE_VERSION}" "pmd_releases/${RELEASE_VERSION}"。如果发布早期(如 maven central 部署第一步)失败,脚本还给出了"删除标签并用 --force 重新推送"的补救路径,但明确指出一旦制品已推送到 Maven Central,这种做法就会破坏可复现构建(reproducible builds)。

总结

可以把 PMD 的版本策略浓缩为三条可操作的结论:

  1. 补丁看 bug、次版本看规则、主版本看迁移:patch 只修 bug 和安全问题,绝对可替换;minor 可能因 FN 修复或新规则让构建失败,需要留意 release notes 中的规则变更;major 允许破坏一切,必须走 migrating_to_pmd7.md 这类迁移指南。
  2. 废弃是缓冲带,移除只发生在 major:无论是 API、规则还是属性,PMD 都坚持"先废弃、再引用新对象、直到下一个主版本才移除",配合 @Deprecated/@Experimental/@InternalApi 注解(见 adr-3.md 与 rule_deprecation.md),让自定义 ruleset 在升级过程中尽量保持可用。
  3. 发布从 main 直接打标签:标签统一命名为 pmd_releases/MAJOR.MINOR.PATCH,patch 发布在 main 有开发进行时从上一标签切分支,发布后的构建与分发由 GitHub Actions 自动完成(github_actions_workflows.md)。

结合 support_lifecycle.md 中"只有最新主版本处于活跃开发"的规则,可以得出一个清晰的升级建议:始终跟随最新主版本的最新次版本/补丁版本,才能在获得新特性与修复的同时,把升级成本与意外风险控制在可预期的范围内。

登录后查看全文
pmd